@bridge_gpt/mcp-server 0.2.21 → 0.2.24
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 +144 -18
- 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/decision-page-template.js +9 -4
- package/build/docs.generated.js +5 -0
- 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 +2741 -702
- package/build/init.js +29 -0
- package/build/install-bridge.js +1076 -114
- package/build/pipelines.generated.js +2 -2
- package/build/pr-base-contract.js +36 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +483 -0
- package/build/sfcc/log-gate.js +85 -0
- package/build/sfcc/log-query.js +170 -0
- package/build/sfcc/register.js +10 -0
- package/build/sfcc/setup-status.js +33 -3
- package/build/start-tickets.js +164 -75
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +62 -10
- package/{CONDUCTOR.md → docs/CONDUCTOR.md} +88 -29
- package/docs/install/github-app.md +189 -0
- package/docs/install/mcp-tool-integrations.md +305 -0
- package/docs/install/sfcc-integration.md +140 -0
- package/package.json +5 -5
- package/public/js/main.min.js +55 -10
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +3 -2
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sfcc_log_query` — gated, on-demand SFCC log-query MCP tool (T7 / BAPI-556).
|
|
3
|
+
*
|
|
4
|
+
* A thin, profile-gated handler that forwards an explicitly bounded query to the
|
|
5
|
+
* Bridge backend, which runs T1 pull → T2 redaction → T3 filtering server-side
|
|
6
|
+
* and returns scoped, redacted findings. `environment` and `time_range` are
|
|
7
|
+
* REQUIRED with no broad defaults; entries, time range, and log-file prefixes are
|
|
8
|
+
* all capped to prevent runaway retrieval. Basic-auth WebDAV credentials never
|
|
9
|
+
* leave the backend — this tool holds none.
|
|
10
|
+
*
|
|
11
|
+
* The backend response has already passed through the `RedactionPort`, so its JSON
|
|
12
|
+
* is returned to the LLM unchanged; there is deliberately NO local retrieval
|
|
13
|
+
* bypass in TypeScript.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { withSfccLogGate } from "./log-gate.js";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Bounds — mirror src/python/sfcc_monitor/log_query.py (kept in lock-step).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const MAX_QUERY_RANGE_HOURS = 24;
|
|
21
|
+
const HIGH_VOLUME_MAX_RANGE_HOURS = 6;
|
|
22
|
+
const HIGH_VOLUME_PREFIXES = new Set(["info", "jobs", "debug", "customdebug"]);
|
|
23
|
+
const MAX_SELECTED_PREFIXES = 5;
|
|
24
|
+
const MAX_MAX_ENTRIES = 2000;
|
|
25
|
+
const SUPPORTED_ENVIRONMENTS = ["production", "staging", "development"];
|
|
26
|
+
const PREFIX_RE = /^[a-z][a-z0-9]*$/;
|
|
27
|
+
function jsonResult(payload) {
|
|
28
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
29
|
+
}
|
|
30
|
+
function validationError(message) {
|
|
31
|
+
return jsonResult({ error: "VALIDATION_ERROR", status: 400, message });
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Zod input schema
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
const inputSchema = z.object({
|
|
37
|
+
environment: z
|
|
38
|
+
.enum(SUPPORTED_ENVIRONMENTS)
|
|
39
|
+
.describe("REQUIRED environment to scope the query to. Deliberately required so a " +
|
|
40
|
+
"query can never fan across every environment at once."),
|
|
41
|
+
time_range: z
|
|
42
|
+
.object({
|
|
43
|
+
start: z.string().datetime({ offset: true }).describe("Inclusive ISO-8601 UTC start."),
|
|
44
|
+
end: z.string().datetime({ offset: true }).describe("Exclusive ISO-8601 UTC end (after start)."),
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.describe("REQUIRED bounded window. No open-ended or inferred period is ever assumed."),
|
|
48
|
+
prefixes: z
|
|
49
|
+
.array(z.string().regex(PREFIX_RE, "prefix must be a letter followed by letters/digits"))
|
|
50
|
+
.max(MAX_SELECTED_PREFIXES)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe(`Optional log-file prefix selection (max ${MAX_SELECTED_PREFIXES}). Empty = the ` +
|
|
53
|
+
"shipped error-class defaults. High-volume prefixes (info/jobs/debug/customdebug) " +
|
|
54
|
+
`impose a stricter ${HIGH_VOLUME_MAX_RANGE_HOURS}h max time range.`),
|
|
55
|
+
max_entries: z
|
|
56
|
+
.number()
|
|
57
|
+
.int()
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(MAX_MAX_ENTRIES)
|
|
60
|
+
.optional()
|
|
61
|
+
.describe(`Optional per-query entry-scan cap (1..${MAX_MAX_ENTRIES}).`),
|
|
62
|
+
});
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Handler
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Perform handler-level semantic checks so agents get a structured
|
|
68
|
+
* VALIDATION_ERROR before any network retrieval where possible.
|
|
69
|
+
*/
|
|
70
|
+
function semanticCheck(args) {
|
|
71
|
+
const start = Date.parse(args.time_range.start);
|
|
72
|
+
const end = Date.parse(args.time_range.end);
|
|
73
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
74
|
+
return "time_range.start and time_range.end must be valid ISO-8601 timestamps.";
|
|
75
|
+
}
|
|
76
|
+
if (!(start < end)) {
|
|
77
|
+
return "time_range.start must be strictly before time_range.end.";
|
|
78
|
+
}
|
|
79
|
+
const prefixes = args.prefixes ?? [];
|
|
80
|
+
const hasHighVolume = prefixes.some((p) => HIGH_VOLUME_PREFIXES.has(p));
|
|
81
|
+
const maxHours = hasHighVolume ? HIGH_VOLUME_MAX_RANGE_HOURS : MAX_QUERY_RANGE_HOURS;
|
|
82
|
+
const spanHours = (end - start) / 3_600_000;
|
|
83
|
+
if (spanHours > maxHours) {
|
|
84
|
+
return (`time_range spans ${spanHours.toFixed(1)}h but the maximum for this prefix ` +
|
|
85
|
+
`selection is ${maxHours}h. environment and time_range are required, and the ` +
|
|
86
|
+
"range is capped, to prevent broad cross-environment or open-ended log scans.");
|
|
87
|
+
}
|
|
88
|
+
// Uniqueness (Zod validates grammar + count; this catches duplicates).
|
|
89
|
+
if (new Set(prefixes).size !== prefixes.length) {
|
|
90
|
+
return "prefixes must be unique.";
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function buildHandler(deps) {
|
|
95
|
+
return async (rawArgs) => {
|
|
96
|
+
const parsed = inputSchema.safeParse(rawArgs);
|
|
97
|
+
if (!parsed.success) {
|
|
98
|
+
return validationError(parsed.error.issues.map((i) => i.message).join("; "));
|
|
99
|
+
}
|
|
100
|
+
const args = parsed.data;
|
|
101
|
+
const semanticFailure = semanticCheck(args);
|
|
102
|
+
if (semanticFailure)
|
|
103
|
+
return validationError(semanticFailure);
|
|
104
|
+
const body = {
|
|
105
|
+
repo_name: deps.repoName,
|
|
106
|
+
environment: args.environment,
|
|
107
|
+
time_range: { start: args.time_range.start, end: args.time_range.end },
|
|
108
|
+
prefixes: args.prefixes ?? [],
|
|
109
|
+
...(args.max_entries !== undefined ? { max_entries: args.max_entries } : {}),
|
|
110
|
+
};
|
|
111
|
+
let resp;
|
|
112
|
+
try {
|
|
113
|
+
const url = deps.buildGetUrl("/sfcc/logs/query", { repo_name: deps.repoName });
|
|
114
|
+
resp = await fetch(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: await deps.getPostHeaders(),
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return jsonResult({
|
|
122
|
+
error: "BAD_GATEWAY",
|
|
123
|
+
status: 502,
|
|
124
|
+
message: "Could not reach Bridge API to run the SFCC log query. Check that " +
|
|
125
|
+
"BAPI_BASE_URL points to a running Bridge API instance.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const text = await resp.text();
|
|
129
|
+
if (!resp.ok) {
|
|
130
|
+
let detail = text;
|
|
131
|
+
try {
|
|
132
|
+
detail = JSON.parse(text);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* keep raw text detail */
|
|
136
|
+
}
|
|
137
|
+
return jsonResult({
|
|
138
|
+
error: resp.status >= 500 ? "SERVICE_UNAVAILABLE" : "REQUEST_FAILED",
|
|
139
|
+
status: resp.status,
|
|
140
|
+
detail,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// The backend response has already passed through RedactionPort — return it
|
|
144
|
+
// unchanged as the MCP text payload (bounded server-side; no local persistence).
|
|
145
|
+
return { content: [{ type: "text", text }] };
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Registration
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
/**
|
|
152
|
+
* Register the `sfcc_log_query` tool through the host's registerTool wrapper.
|
|
153
|
+
* Called only inside the `deps.includeReadTools` SFCC-profile branch.
|
|
154
|
+
*/
|
|
155
|
+
export function registerSfccLogQueryTool(registerTool, deps) {
|
|
156
|
+
const gated = withSfccLogGate({ buildGetUrl: deps.buildGetUrl, getGetHeaders: deps.getGetHeaders, repoName: deps.repoName }, buildHandler(deps));
|
|
157
|
+
registerTool("sfcc_log_query", {
|
|
158
|
+
description: "Query redacted, filtered SFCC logs on demand, scoped to a REQUIRED environment " +
|
|
159
|
+
"and time_range. Runs pull→redact→filter on the Bridge backend; entries, time range, " +
|
|
160
|
+
"and log-file prefixes are capped (high-volume prefixes get a stricter range). Returns " +
|
|
161
|
+
"a NOT_CONFIGURED 503 when the log capability isn't set up — run sfcc_setup_status.",
|
|
162
|
+
inputSchema,
|
|
163
|
+
annotations: {
|
|
164
|
+
readOnlyHint: true,
|
|
165
|
+
destructiveHint: false,
|
|
166
|
+
idempotentHint: true,
|
|
167
|
+
openWorldHint: true,
|
|
168
|
+
},
|
|
169
|
+
}, gated);
|
|
170
|
+
}
|
package/build/sfcc/register.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerSystemObjectReadTools } from "./reads-system-object.js";
|
|
|
14
14
|
import { registerSfccCustomObjectDefReadTools } from "./reads-custom-object-def.js";
|
|
15
15
|
import { registerSitePreferenceTools } from "./reads-site-preference.js";
|
|
16
16
|
import { registerSfccWriteTools } from "./writes.js";
|
|
17
|
+
import { registerSfccLogQueryTool } from "./log-query.js";
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Registration
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -78,5 +79,14 @@ export function registerSfccTools(registerTool, deps) {
|
|
|
78
79
|
gateDeps,
|
|
79
80
|
getDocsDir: deps.getDocsDir,
|
|
80
81
|
});
|
|
82
|
+
// sfcc_log_query (BAPI-556 T7) — gated, on-demand redacted/filtered log query.
|
|
83
|
+
// Uses its OWN call-time gate (log capability = WebDAV Basic auth), NOT the
|
|
84
|
+
// OCAPI version/dw.json/AM-token gate above.
|
|
85
|
+
registerSfccLogQueryTool(registerTool, {
|
|
86
|
+
buildGetUrl: deps.buildGetUrl,
|
|
87
|
+
getGetHeaders: deps.getGetHeaders,
|
|
88
|
+
getPostHeaders: deps.getPostHeaders,
|
|
89
|
+
repoName: deps.repoName,
|
|
90
|
+
});
|
|
81
91
|
}
|
|
82
92
|
}
|
|
@@ -19,7 +19,8 @@ import { getAmToken } from "./client.js";
|
|
|
19
19
|
* 2. Repo name set (REPO_NAME)
|
|
20
20
|
* 3. version config field is an SFCC version
|
|
21
21
|
* 4. dw.json found / instance unambiguous
|
|
22
|
-
* 5. AM token acquisition
|
|
22
|
+
* 5. AM token acquisition (OCAPI Account Manager — NOT log/WebDAV access)
|
|
23
|
+
* 6. SFCC Log Query capability (WebDAV Basic auth — independent of steps 3–5)
|
|
23
24
|
*
|
|
24
25
|
* Never throws; each check is caught independently so partial states are
|
|
25
26
|
* always reported. Output is completely secret-free.
|
|
@@ -97,8 +98,37 @@ export async function sfccSetupStatusTool(deps) {
|
|
|
97
98
|
tokenStatus = `✗ ${msg}`;
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
lines.push(`5. AM Token: ${tokenStatus}`);
|
|
101
|
-
|
|
101
|
+
lines.push(`5. AM Token (OCAPI): ${tokenStatus}`);
|
|
102
|
+
// 6. SFCC Log Query capability — WebDAV Basic auth, independent of OCAPI/AM.
|
|
103
|
+
// A single Bridge backend probe (secret-free) reports readiness. This is a
|
|
104
|
+
// separate credential surface: an SFCC repo can have OCAPI working (steps 3–5)
|
|
105
|
+
// while log/WebDAV access is not configured, and vice versa.
|
|
106
|
+
let logQueryStatus = "— Skipped (Bridge API not configured)";
|
|
107
|
+
if (apiKeyOk && repoOk) {
|
|
108
|
+
try {
|
|
109
|
+
const url = deps.buildGetUrl("/sfcc/logs/capability", { repo_name: deps.repoName });
|
|
110
|
+
const resp = await fetch(url, { headers: await deps.getGetHeaders() });
|
|
111
|
+
if (!resp.ok) {
|
|
112
|
+
logQueryStatus = `✗ Could not read (Bridge API ${resp.status})`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const body = (await resp.json());
|
|
116
|
+
if (body?.configured === true) {
|
|
117
|
+
logQueryStatus = "✓ Configured (WebDAV log access ready)";
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const msg = typeof body?.message === "string" ? body.message : "Not configured";
|
|
121
|
+
logQueryStatus = `✗ ${msg}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
logQueryStatus = `✗ Resolution error: ${err instanceof Error ? err.message : String(err)}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
lines.push(`6. SFCC Log Query (WebDAV): ${logQueryStatus}`);
|
|
130
|
+
lines.push("\nRun `check_permissions` to probe OCAPI access once steps 1–5 are all green. " +
|
|
131
|
+
"Step 6 (log/WebDAV access) is independent and gates `sfcc_log_query`.");
|
|
102
132
|
return {
|
|
103
133
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
104
134
|
};
|
package/build/start-tickets.js
CHANGED
|
@@ -71,6 +71,7 @@ import { WORKTRUNK_BINARY_OVERRIDE_ENV, WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERS
|
|
|
71
71
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, resolveModelAlias, isValidModelAlias, isModelTier, } from "./agent-registry.js";
|
|
72
72
|
import { createStartTicketsConductorContext, provisionConductorHooksForRows, emitStartTicketsRunStarted, injectConductorEnvIntoShellCommand, buildSupervisorTabCommand, isSupervisorLaunchEnabled, supervisorSpawnKey, } from "./start-tickets-conductor.js";
|
|
73
73
|
import { transitionEpicDispatch, resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
74
|
+
import { PR_BASE_BRANCH_ENV_VAR, buildPrBaseContractLaunchInstruction, } from "./pr-base-contract.js";
|
|
74
75
|
// Re-export the shared prereq surface (constants, platform helpers, command
|
|
75
76
|
// probes) so existing import sites that read them from "./start-tickets.js"
|
|
76
77
|
// keep working unchanged.
|
|
@@ -101,6 +102,8 @@ export function getStartTicketsUsage() {
|
|
|
101
102
|
"",
|
|
102
103
|
"Flags:",
|
|
103
104
|
" --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)",
|
|
105
|
+
" --workflow implement|review-and-implement Slash command each spawned worktree runs (default: implement). review-and-implement runs /review-ticket then, after a per-ticket halt gate, /implement-ticket in the same session; --auto applies to the selected workflow.",
|
|
106
|
+
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
104
107
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
105
108
|
" --dry-run Print intended actions; creates no worktrees and opens no tabs, but DOES resolve model routing read-only (may compute+cache a ticket's difficulty) to preview the --model each tab would use",
|
|
106
109
|
" --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)",
|
|
@@ -151,6 +154,8 @@ export function parseStartTicketsArgs(argv) {
|
|
|
151
154
|
let agentName = DEFAULT_AGENT_NAME;
|
|
152
155
|
let baseBranch = "main";
|
|
153
156
|
let conductorEnabled = false;
|
|
157
|
+
let workflow = "implement";
|
|
158
|
+
let reviewRoundsRaw;
|
|
154
159
|
const branchEntries = [];
|
|
155
160
|
const keys = [];
|
|
156
161
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -183,6 +188,49 @@ export function parseStartTicketsArgs(argv) {
|
|
|
183
188
|
agentName = value;
|
|
184
189
|
continue;
|
|
185
190
|
}
|
|
191
|
+
if (arg === "--workflow" || arg.startsWith("--workflow=")) {
|
|
192
|
+
let value;
|
|
193
|
+
if (arg.startsWith("--workflow=")) {
|
|
194
|
+
value = arg.slice("--workflow=".length);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
value = takeValue();
|
|
198
|
+
if (value === undefined) {
|
|
199
|
+
return {
|
|
200
|
+
status: "error",
|
|
201
|
+
message: "--workflow requires a value (allowed values: implement, review-and-implement).",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (value !== "implement" && value !== "review-and-implement") {
|
|
206
|
+
return {
|
|
207
|
+
status: "error",
|
|
208
|
+
message: `Invalid --workflow value: '${value}' (allowed values: implement, review-and-implement).`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
workflow = value;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (arg === "--rounds" || arg.startsWith("--rounds=")) {
|
|
215
|
+
let value;
|
|
216
|
+
if (arg.startsWith("--rounds=")) {
|
|
217
|
+
value = arg.slice("--rounds=".length);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
value = takeValue();
|
|
221
|
+
if (value === undefined) {
|
|
222
|
+
return { status: "error", message: "--rounds requires a value (allowed values: 1, 2)." };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (value !== "1" && value !== "2") {
|
|
226
|
+
return {
|
|
227
|
+
status: "error",
|
|
228
|
+
message: `Invalid --rounds value: '${value}' (allowed values: 1, 2).`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
reviewRoundsRaw = value;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
186
234
|
if (arg === "--terminal" || arg.startsWith("--terminal=")) {
|
|
187
235
|
let value;
|
|
188
236
|
if (arg.startsWith("--terminal=")) {
|
|
@@ -337,29 +385,38 @@ export function parseStartTicketsArgs(argv) {
|
|
|
337
385
|
}
|
|
338
386
|
branchOverrides[overrideKey] = branchName;
|
|
339
387
|
}
|
|
388
|
+
// --- rounds validation: review-only, checked after the full argument scan
|
|
389
|
+
// so flag order (e.g. --rounds before --workflow) never affects validation.
|
|
390
|
+
let reviewRounds;
|
|
391
|
+
if (reviewRoundsRaw !== undefined) {
|
|
392
|
+
if (workflow !== "review-and-implement") {
|
|
393
|
+
return {
|
|
394
|
+
status: "error",
|
|
395
|
+
message: "--rounds is only valid with --workflow review-and-implement.",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
reviewRounds = reviewRoundsRaw === "1" ? 1 : 2;
|
|
399
|
+
}
|
|
340
400
|
return {
|
|
341
401
|
status: "ok",
|
|
342
|
-
options: {
|
|
402
|
+
options: {
|
|
403
|
+
keys,
|
|
404
|
+
terminal,
|
|
405
|
+
dryRun,
|
|
406
|
+
autoApprove,
|
|
407
|
+
refreshMain,
|
|
408
|
+
maxParallel,
|
|
409
|
+
branchOverrides,
|
|
410
|
+
agentName,
|
|
411
|
+
baseBranch,
|
|
412
|
+
conductorEnabled,
|
|
413
|
+
workflow,
|
|
414
|
+
reviewRounds,
|
|
415
|
+
},
|
|
343
416
|
};
|
|
344
417
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (branch.trim().length === 0)
|
|
348
|
-
return "branch name must not be empty.";
|
|
349
|
-
if (branch.length > 255)
|
|
350
|
-
return "branch name must be 255 characters or fewer.";
|
|
351
|
-
if (branch.startsWith("-"))
|
|
352
|
-
return "branch name must not start with '-'.";
|
|
353
|
-
// Reject ASCII control characters (0x00-0x1F and 0x7F) without embedding
|
|
354
|
-
// raw control bytes in source.
|
|
355
|
-
for (let i = 0; i < branch.length; i++) {
|
|
356
|
-
const code = branch.charCodeAt(i);
|
|
357
|
-
if (code <= 0x1f || code === 0x7f) {
|
|
358
|
-
return "branch name must not contain control characters.";
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
418
|
+
// `validateBranchName` moved to `base-ref.ts` (BAPI-586); imported + re-exported
|
|
419
|
+
// above. See the import block near the worktree-core re-exports.
|
|
363
420
|
/**
|
|
364
421
|
* Determine which macOS terminal to drive. An explicit choice wins; otherwise
|
|
365
422
|
* auto-detect iTerm from `$TERM_PROGRAM` (case-insensitive), defaulting to
|
|
@@ -403,19 +460,27 @@ export function getDefaultSpawnTerminalTabForPlatform(platform) {
|
|
|
403
460
|
export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = false, conductorEnabled = false, repoName = null,
|
|
404
461
|
// BAPI-494: a conductor remediation re-dispatch. Appends the full-suite finalize
|
|
405
462
|
// instruction to the resume-mode worker's prompt.
|
|
406
|
-
resumeMode = false
|
|
463
|
+
resumeMode = false, workflow = "implement", reviewRounds,
|
|
464
|
+
// BAPI-586: the effective run base branch (incl. epic.base_branch override).
|
|
465
|
+
// Injected as BAPI_BASE_BRANCH for conductor workers so their PR targets it,
|
|
466
|
+
// and threaded (BAPI-593) into the spawned workflow command.
|
|
467
|
+
baseBranch) {
|
|
407
468
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
408
469
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
409
470
|
}
|
|
410
471
|
const platform = deps.platform;
|
|
472
|
+
// Only conductor dispatch uses the PR-base contract; interactive dispatch omits
|
|
473
|
+
// the BAPI_BASE_BRANCH assignment so its worker launch is unchanged.
|
|
474
|
+
const prBaseBranch = conductorEnabled ? baseBranch : null;
|
|
411
475
|
return {
|
|
412
476
|
ok: true,
|
|
413
477
|
config: {
|
|
414
478
|
platform,
|
|
415
479
|
worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
|
|
416
480
|
// Inject the resolved repo identity so the spawned worktree session never
|
|
417
|
-
// falls back to the basename-derived repo name (the 403 root cause)
|
|
418
|
-
|
|
481
|
+
// falls back to the basename-derived repo name (the 403 root cause), and
|
|
482
|
+
// (BAPI-586) the run base so the conductor worker opens its PR against it.
|
|
483
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch), repoName, platform), prBaseBranch, platform),
|
|
419
484
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
420
485
|
},
|
|
421
486
|
};
|
|
@@ -445,6 +510,24 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
|
|
|
445
510
|
}
|
|
446
511
|
return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
|
|
447
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* BAPI-586: prepend a `BAPI_BASE_BRANCH` environment assignment to a spawned
|
|
515
|
+
* conductor worker's shell command so the worker can open its PR against the run
|
|
516
|
+
* base via `gh pr create --base "$BAPI_BASE_BRANCH"` (paired with the PR-base
|
|
517
|
+
* launch instruction), instead of inferring the repo default branch. Platform
|
|
518
|
+
* correct — `$env:VAR = '…'; …` on PowerShell, `export VAR='…' && …` on POSIX —
|
|
519
|
+
* with the value quoted by the same escaper used for the rest of the command.
|
|
520
|
+
* Fail-open: a null/empty `baseBranch` returns the command unchanged (interactive
|
|
521
|
+
* dispatch does not use the PR-base contract).
|
|
522
|
+
*/
|
|
523
|
+
export function prependBaseBranchEnvAssignment(command, baseBranch, platform = "darwin") {
|
|
524
|
+
if (!baseBranch)
|
|
525
|
+
return command;
|
|
526
|
+
if (platform === "win32") {
|
|
527
|
+
return `$env:${PR_BASE_BRANCH_ENV_VAR} = ${powershellSquote(baseBranch)}; ${command}`;
|
|
528
|
+
}
|
|
529
|
+
return `export ${PR_BASE_BRANCH_ENV_VAR}='${shSquoteInner(baseBranch)}' && ${command}`;
|
|
530
|
+
}
|
|
448
531
|
/**
|
|
449
532
|
* Escape a string for inclusion inside a single-quoted shell context: each
|
|
450
533
|
* embedded single-quote becomes the sequence `'\''`. Returns only the inner
|
|
@@ -725,42 +808,10 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
725
808
|
export async function refreshMainBranch(deps, options) {
|
|
726
809
|
return refreshBaseBranch(deps, { refreshMain: options.refreshMain, baseBranch: "main" });
|
|
727
810
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
* `git merge --ff-only`, no `git branch --force`). Used by review-grounding
|
|
733
|
-
* (BAPI-474), which must read a pinned base tree without touching the user's
|
|
734
|
-
* working tree, index, stash, or local branch refs.
|
|
735
|
-
*
|
|
736
|
-
* Validates `baseBranch` with {@link validateBranchName} first so an
|
|
737
|
-
* injection-shaped ref name is rejected before any git invocation. Every git
|
|
738
|
-
* call uses an argv array (never a shell string), consistent with
|
|
739
|
-
* {@link refreshBaseBranch}.
|
|
740
|
-
*/
|
|
741
|
-
export async function fetchAndResolveBaseSha(deps, baseBranch) {
|
|
742
|
-
const validationError = validateBranchName(baseBranch);
|
|
743
|
-
if (validationError) {
|
|
744
|
-
return { ok: false, error: `Invalid base branch '${baseBranch}': ${validationError}` };
|
|
745
|
-
}
|
|
746
|
-
const fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
|
|
747
|
-
cwd: deps.cwd,
|
|
748
|
-
});
|
|
749
|
-
if (!commandSucceeded(fetch)) {
|
|
750
|
-
return {
|
|
751
|
-
ok: false,
|
|
752
|
-
error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`,
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
const resolve = await deps.runCommand("git", ["rev-parse", "--verify", `origin/${baseBranch}^{commit}`], { cwd: deps.cwd });
|
|
756
|
-
if (!commandSucceeded(resolve)) {
|
|
757
|
-
return {
|
|
758
|
-
ok: false,
|
|
759
|
-
error: `Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`,
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
return { ok: true, base_sha: resolve.stdout.trim() };
|
|
763
|
-
}
|
|
811
|
+
// `fetchAndResolveBaseSha` + `FetchAndResolveBaseShaResult` moved to
|
|
812
|
+
// `base-ref.ts` (BAPI-586) and re-exported above. The shared helper now also
|
|
813
|
+
// serializes the fetch per repository via an async mutex so concurrent executor
|
|
814
|
+
// fresh jobs against one clone cannot collide on git lock files.
|
|
764
815
|
// ---------------------------------------------------------------------------
|
|
765
816
|
// Concurrency + worktree creation
|
|
766
817
|
// ---------------------------------------------------------------------------
|
|
@@ -798,6 +849,13 @@ export async function runWithConcurrency(items, limit, worker) {
|
|
|
798
849
|
// same function references.
|
|
799
850
|
import { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, } from "./worktree-core.js";
|
|
800
851
|
export { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, };
|
|
852
|
+
// BAPI-586: base-ref resolution now lives in the focused `base-ref.ts` module so
|
|
853
|
+
// the executor can depend on it without pulling in this large CLI module. We
|
|
854
|
+
// import the VALUES here (so internal uses keep working) and re-export the same
|
|
855
|
+
// references so existing importers of `./start-tickets.js` (index.ts,
|
|
856
|
+
// review-tickets.ts) and the pinned start-tickets tests are unaffected.
|
|
857
|
+
import { validateBranchName, fetchAndResolveBaseSha } from "./base-ref.js";
|
|
858
|
+
export { validateBranchName, fetchAndResolveBaseSha };
|
|
801
859
|
/**
|
|
802
860
|
* Create / switch worktrees for every ticket, throttled to `maxParallel`, using
|
|
803
861
|
* the resolved Worktrunk binary. Returns one row per ticket in original key
|
|
@@ -811,7 +869,17 @@ export { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlat
|
|
|
811
869
|
* historical branch-name behavior unchanged.
|
|
812
870
|
*/
|
|
813
871
|
export async function createWorktrees(deps, options, worktrunkBinary, baseStartPoint = options.baseBranch) {
|
|
814
|
-
|
|
872
|
+
// BAPI-586: for guard-enabled Conductor fresh dispatch the caller has already
|
|
873
|
+
// resolved `baseStartPoint` to an immutable base SHA (`nonMutatingBase`). Align
|
|
874
|
+
// any safe pre-existing branch exactly to that SHA and verify the resulting
|
|
875
|
+
// head against it, so a dependent ticket starts at fresh `origin/<base>` rather
|
|
876
|
+
// than at a stale ancestor or a sibling seed. Interactive dispatch (a moving
|
|
877
|
+
// branch name, guard off) keeps the historical reuse-as-is behavior.
|
|
878
|
+
const exactBase = options.guardStaleWorktree === true && options.nonMutatingBase === true;
|
|
879
|
+
const behavior = exactBase
|
|
880
|
+
? { alignExistingBranchTo: baseStartPoint, verifyHeadMatches: baseStartPoint }
|
|
881
|
+
: {};
|
|
882
|
+
return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, baseStartPoint, options.guardStaleWorktree === true, behavior));
|
|
815
883
|
}
|
|
816
884
|
/**
|
|
817
885
|
* Resume-mode worktree resolution (BAPI-441). Instead of creating worktrees,
|
|
@@ -931,10 +999,26 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
931
999
|
// `modelAlias` is accepted for signature consistency only — the model is
|
|
932
1000
|
// injected as a `--model` flag (see buildAgentInvocationArgv), never embedded
|
|
933
1001
|
// in the prompt text.
|
|
934
|
-
const
|
|
1002
|
+
const workflow = opts.workflow ?? "implement";
|
|
1003
|
+
const head = workflow === "review-and-implement" ? "/review-and-implement" : "/implement-ticket";
|
|
1004
|
+
let command = `${head} ${key}${opts.autoApprove ? " --auto" : ""}`;
|
|
1005
|
+
// Review-only arguments (`--rounds`, non-default `--base-branch`) must never
|
|
1006
|
+
// leak into the legacy `/implement-ticket` prompt.
|
|
1007
|
+
if (workflow === "review-and-implement") {
|
|
1008
|
+
if (opts.reviewRounds !== undefined) {
|
|
1009
|
+
command += ` --rounds=${opts.reviewRounds}`;
|
|
1010
|
+
}
|
|
1011
|
+
if (opts.baseBranch !== undefined && opts.baseBranch !== "main") {
|
|
1012
|
+
command += ` --base-branch='${shSquoteInner(opts.baseBranch)}'`;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
935
1015
|
const parts = [command];
|
|
936
|
-
if (opts.conductorEnabled)
|
|
1016
|
+
if (opts.conductorEnabled) {
|
|
937
1017
|
parts.push(buildConductorMessageRelayLaunchInstruction());
|
|
1018
|
+
// BAPI-586: conductor implementation workers must open their PR against the
|
|
1019
|
+
// run base (injected as BAPI_BASE_BRANCH), never the repo default branch.
|
|
1020
|
+
parts.push(buildPrBaseContractLaunchInstruction());
|
|
1021
|
+
}
|
|
938
1022
|
if (opts.resumeMode)
|
|
939
1023
|
parts.push(buildResumeModeRemediationFinalizeInstruction());
|
|
940
1024
|
return parts.join(" ");
|
|
@@ -976,13 +1060,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias) {
|
|
|
976
1060
|
}
|
|
977
1061
|
}
|
|
978
1062
|
/** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
|
|
979
|
-
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
|
|
980
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }), (p) => `'${shSquoteInner(p)}'`, modelAlias);
|
|
1063
|
+
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
|
|
1064
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch }), (p) => `'${shSquoteInner(p)}'`, modelAlias);
|
|
981
1065
|
return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
|
|
982
1066
|
}
|
|
983
1067
|
/** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
|
|
984
|
-
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
|
|
985
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode }), powershellSquote, modelAlias);
|
|
1068
|
+
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
|
|
1069
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch }), powershellSquote, modelAlias);
|
|
986
1070
|
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
|
|
987
1071
|
}
|
|
988
1072
|
/**
|
|
@@ -993,10 +1077,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
|
|
|
993
1077
|
* injected as `--model` at the spawn boundary. `conductorEnabled` appends the
|
|
994
1078
|
* BAPI-397 message-relay instruction to the prompt (opt-in via `--conductor`).
|
|
995
1079
|
*/
|
|
996
|
-
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false) {
|
|
1080
|
+
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
|
|
997
1081
|
if (platform === "win32")
|
|
998
|
-
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
|
|
999
|
-
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode);
|
|
1082
|
+
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch);
|
|
1083
|
+
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch);
|
|
1000
1084
|
}
|
|
1001
1085
|
/**
|
|
1002
1086
|
* Build the shell command run inside a spawned tab/session for an ARBITRARY
|
|
@@ -1518,14 +1602,16 @@ export function buildDryRunResults(keys, overrides) {
|
|
|
1518
1602
|
* PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
|
|
1519
1603
|
* POSIX fallback for unsupported platforms.
|
|
1520
1604
|
*/
|
|
1521
|
-
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null) {
|
|
1605
|
+
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch) {
|
|
1522
1606
|
return {
|
|
1523
1607
|
worktrunkBinary: resolveWorktrunkBinary(platform, env),
|
|
1524
1608
|
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
1525
1609
|
// now passes the previewed tier's alias so `--model` shows in the preview.
|
|
1526
1610
|
// The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix
|
|
1527
|
-
// so the dry-run preview matches the real spawn command exactly.
|
|
1528
|
-
|
|
1611
|
+
// so the dry-run preview matches the real spawn command exactly. Reuses the
|
|
1612
|
+
// same buildAgentShellCommand/buildAgentPrompt path as a real spawn — dry-run
|
|
1613
|
+
// is never special-cased — so the previewed workflow prompt is exact.
|
|
1614
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias) => prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch), repoName, platform),
|
|
1529
1615
|
};
|
|
1530
1616
|
}
|
|
1531
1617
|
/**
|
|
@@ -1563,8 +1649,8 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
1563
1649
|
* the secret-free MCP provisioning preview. Pure platform formatting only — no
|
|
1564
1650
|
* preflight, no routing failures.
|
|
1565
1651
|
*/
|
|
1566
|
-
export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation) {
|
|
1567
|
-
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName);
|
|
1652
|
+
export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds) {
|
|
1653
|
+
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
|
|
1568
1654
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
1569
1655
|
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
1570
1656
|
return [
|
|
@@ -2504,7 +2590,10 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
2504
2590
|
}
|
|
2505
2591
|
const platformConfig = resolveStartTicketsPlatformConfig(deps, agent, options.autoApprove, options.conductorEnabled ?? false, resolvedRepoName,
|
|
2506
2592
|
// BAPI-494: resume-mode dispatches get the full-suite remediation finalize prompt.
|
|
2507
|
-
options.resumeMode ?? false
|
|
2593
|
+
options.resumeMode ?? false, options.workflow, options.reviewRounds,
|
|
2594
|
+
// BAPI-586: the effective run base (already carries any epic.base_branch
|
|
2595
|
+
// override applied above) so conductor workers get BAPI_BASE_BRANCH.
|
|
2596
|
+
options.baseBranch);
|
|
2508
2597
|
if (!platformConfig.ok)
|
|
2509
2598
|
return { ok: false, error: platformConfig.error };
|
|
2510
2599
|
// BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
|
|
@@ -2707,7 +2796,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
2707
2796
|
const branch = resolveBranchForTicket(key, options.branchOverrides);
|
|
2708
2797
|
const routedRow = routedByKey.get(key);
|
|
2709
2798
|
const modelAlias = routedRow?.modelAlias ?? null;
|
|
2710
|
-
for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation)) {
|
|
2799
|
+
for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds)) {
|
|
2711
2800
|
log(line);
|
|
2712
2801
|
}
|
|
2713
2802
|
log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent)}`);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
2
|
+
export const VERSION = "0.2.24";
|