@bridge_gpt/mcp-server 0.2.49 → 0.2.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +24 -7
  2. package/build/base-ref.js +28 -3
  3. package/build/claude-review-workflow-drift-probe.js +130 -0
  4. package/build/claude-review-workflow-drift.js +173 -0
  5. package/build/claude-review-workflow.js +81 -16
  6. package/build/commands.generated.js +5 -5
  7. package/build/conductor/done-gate.js +25 -3
  8. package/build/conductor/install-doctor.js +65 -5
  9. package/build/conductor/latest-check-selector.js +170 -0
  10. package/build/conductor/local-merge.js +8 -6
  11. package/build/conductor-bin.js +1 -1
  12. package/build/{brainstorm-files.js → council-files.js} +15 -15
  13. package/build/decision-page-schema.js +1 -1
  14. package/build/docs.generated.js +1 -1
  15. package/build/doctor.js +162 -4
  16. package/build/executor/worktree.js +46 -1
  17. package/build/index.js +92 -51
  18. package/build/init.js +9 -2
  19. package/build/install-bridge.js +60 -2
  20. package/build/install-reexec.js +47 -9
  21. package/build/pipelines.generated.js +1 -1
  22. package/build/plane/cli.js +12 -2
  23. package/build/plane/manifest.js +25 -1
  24. package/build/plane/member-roster.js +61 -7
  25. package/build/plane/preflight.js +24 -9
  26. package/build/plane/supervisor.js +77 -5
  27. package/build/plane/types.js +23 -3
  28. package/build/readme.generated.js +1 -1
  29. package/build/run-unit-tests-launcher.js +2 -1
  30. package/build/stale-worktree-doctor.js +120 -0
  31. package/build/start-tickets-prereqs.js +70 -0
  32. package/build/start-tickets.js +91 -3
  33. package/build/version.generated.js +3 -2
  34. package/package.json +4 -2
  35. package/build/chain-orchestrator.js +0 -1457
  36. package/build/chain-utils.js +0 -68
  37. package/build/command-catalog.js +0 -376
  38. package/build/schedule-run.js +0 -1300
  39. package/build/schedule-store.js +0 -172
  40. package/build/scheduled-prompt.js +0 -115
  41. package/build/scheduler-backends/at-fallback.js +0 -139
  42. package/build/scheduler-backends/escaping.js +0 -143
  43. package/build/scheduler-backends/index.js +0 -72
  44. package/build/scheduler-backends/launchd.js +0 -225
  45. package/build/scheduler-backends/systemd-user.js +0 -250
  46. package/build/scheduler-backends/task-scheduler.js +0 -214
  47. package/build/scheduler-backends/types.js +0 -23
@@ -1,225 +0,0 @@
1
- /**
2
- * macOS launchd backend (BAPI-327).
3
- *
4
- * Generates and installs a one-shot LaunchAgent plist that fires the baked
5
- * Claude invocation at a single calendar minute via `StartCalendarInterval`.
6
- *
7
- * Drift policy: the plist omits `RunAtLoad` as defense-in-depth (it avoids an
8
- * immediate run when the agent is reloaded at login), but this is NOT a true
9
- * no-late-fire guarantee — launchd still fires a missed `StartCalendarInterval`
10
- * event on the next wake. Unlike systemd `Persistent=false` / Task Scheduler,
11
- * launchd has no OS-level "skip if missed" knob, so the authoritative staleness
12
- * guard is the baked `--scheduled-at <T>` value in the prompt, which
13
- * `/full-automation` (Phase C) checks before running. The working directory is
14
- * set by the `WorkingDirectory` key, not a Claude flag. `ProgramArguments` is a
15
- * string array (no shell concatenation).
16
- */
17
- import { promises as fs } from "node:fs";
18
- import { pathApiForPlatform } from "./types.js";
19
- import { bakedEnvFromCreateInput, bakedEnvEntries, xmlEscape } from "./escaping.js";
20
- /** launchd label for a NEW schedule id (neutral, generic schedule-run naming). */
21
- export function launchdLabelForId(id) {
22
- return `com.bridge-gpt.schedule-run.${id}`;
23
- }
24
- /**
25
- * Resolve the launchd label for an existing schedule, preferring the label baked
26
- * into the recorded plist path so legacy `com.bridge-gpt.full-automation.<id>`
27
- * schedules remain list/cancel compatible after the neutral-naming change.
28
- */
29
- export function launchdLabelForMetadata(metadata) {
30
- const unitPath = metadata.unit_path;
31
- if (unitPath && unitPath.endsWith(".plist")) {
32
- const base = unitPath.split(/[\\/]/).pop();
33
- if (base)
34
- return base.slice(0, -".plist".length);
35
- }
36
- return launchdLabelForId(metadata.id);
37
- }
38
- /** `~/Library/LaunchAgents/<label>.plist` for a schedule id. */
39
- export function launchdPlistPathForId(id, homeDir) {
40
- const pathApi = pathApiForPlatform("darwin");
41
- return pathApi.join(homeDir, "Library", "LaunchAgents", `${launchdLabelForId(id)}.plist`);
42
- }
43
- /** Convert an ISO timestamp to local launchd calendar fields (minute precision). */
44
- export function startCalendarIntervalFromIso(runAtIso) {
45
- const date = new Date(runAtIso);
46
- return {
47
- Year: date.getFullYear(),
48
- Month: date.getMonth() + 1, // launchd months are 1-12
49
- Day: date.getDate(),
50
- Hour: date.getHours(),
51
- Minute: date.getMinutes(),
52
- };
53
- }
54
- /** UID used by `launchctl` domain targets; falls back to the process UID. */
55
- function resolveUid(deps) {
56
- const fromEnv = deps.env.UID;
57
- if (fromEnv && /^\d+$/.test(fromEnv))
58
- return fromEnv;
59
- const procUid = typeof process.getuid === "function" ? process.getuid() : undefined;
60
- return procUid !== undefined ? String(procUid) : "501";
61
- }
62
- /** Render a valid one-shot launchd plist. */
63
- export function renderLaunchdPlist(input) {
64
- const label = launchdLabelForId(input.id);
65
- const cal = startCalendarIntervalFromIso(input.runAtIso);
66
- const env = bakedEnvEntries(bakedEnvFromCreateInput(input));
67
- // The OS unit runs the Node execution shim (trigger invocation), which then
68
- // spawns the stored agent invocation and records run-history events.
69
- const programArgs = [input.triggerInvocation.exe, ...input.triggerInvocation.args]
70
- .map((arg) => ` <string>${xmlEscape(arg)}</string>`)
71
- .join("\n");
72
- const envEntries = env
73
- .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
74
- .join("\n");
75
- // No run-at-load key (defense-in-depth: avoids an immediate run on agent
76
- // reload). This does NOT stop a missed StartCalendarInterval from firing on
77
- // wake — staleness is enforced by `/full-automation --scheduled-at` (Phase C).
78
- return [
79
- '<?xml version="1.0" encoding="UTF-8"?>',
80
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
81
- '<plist version="1.0">',
82
- "<dict>",
83
- " <key>Label</key>",
84
- ` <string>${xmlEscape(label)}</string>`,
85
- " <key>StartCalendarInterval</key>",
86
- " <dict>",
87
- ` <key>Year</key><integer>${cal.Year}</integer>`,
88
- ` <key>Month</key><integer>${cal.Month}</integer>`,
89
- ` <key>Day</key><integer>${cal.Day}</integer>`,
90
- ` <key>Hour</key><integer>${cal.Hour}</integer>`,
91
- ` <key>Minute</key><integer>${cal.Minute}</integer>`,
92
- " </dict>",
93
- " <key>EnvironmentVariables</key>",
94
- " <dict>",
95
- envEntries,
96
- " </dict>",
97
- " <key>ProgramArguments</key>",
98
- " <array>",
99
- programArgs,
100
- " </array>",
101
- " <key>WorkingDirectory</key>",
102
- ` <string>${xmlEscape(input.repoPath)}</string>`,
103
- " <key>StandardOutPath</key>",
104
- ` <string>${xmlEscape(input.paths.stdoutPath)}</string>`,
105
- " <key>StandardErrorPath</key>",
106
- ` <string>${xmlEscape(input.paths.stderrPath)}</string>`,
107
- "</dict>",
108
- "</plist>",
109
- "",
110
- ].join("\n");
111
- }
112
- /** Create the macOS launchd scheduler backend. */
113
- export function createLaunchdBackend() {
114
- return {
115
- name: "launchd",
116
- async isAvailable(deps) {
117
- return deps.platform === "darwin";
118
- },
119
- async create(input) {
120
- const plistPath = launchdPlistPathForId(input.id, input.deps.homeDir);
121
- const content = renderLaunchdPlist(input);
122
- const artifact = { path: plistPath, content, kind: "launchd-plist" };
123
- if (input.dryRun) {
124
- return {
125
- ok: true,
126
- backend: "launchd",
127
- unitPath: plistPath,
128
- unitPaths: [plistPath],
129
- backendJobId: null,
130
- artifacts: [artifact],
131
- };
132
- }
133
- // ~/Library/LaunchAgents may not exist yet for a fresh user account.
134
- await fs.mkdir(pathApiForPlatform("darwin").dirname(plistPath), { recursive: true });
135
- await fs.writeFile(plistPath, content, "utf-8");
136
- const uid = resolveUid(input.deps);
137
- const result = await input.deps.runCommand("launchctl", [
138
- "bootstrap",
139
- `gui/${uid}`,
140
- plistPath,
141
- ]);
142
- if (result.exitCode !== 0) {
143
- // Don't leave an orphaned plist behind when bootstrap fails.
144
- await fs.unlink(plistPath).catch(() => undefined);
145
- return {
146
- ok: false,
147
- backend: "launchd",
148
- unitPath: plistPath,
149
- unitPaths: [plistPath],
150
- backendJobId: null,
151
- artifacts: [artifact],
152
- error: `launchctl bootstrap failed: ${(result.stderr || result.stdout).trim()}`,
153
- };
154
- }
155
- return {
156
- ok: true,
157
- backend: "launchd",
158
- unitPath: plistPath,
159
- unitPaths: [plistPath],
160
- backendJobId: null,
161
- artifacts: [artifact],
162
- };
163
- },
164
- async list(input) {
165
- const uid = resolveUid(input.deps);
166
- const entries = [];
167
- for (const metadata of input.recorded) {
168
- const plistPath = metadata.unit_path ?? launchdPlistPathForId(metadata.id, input.deps.homeDir);
169
- let exists = true;
170
- try {
171
- await fs.access(plistPath);
172
- }
173
- catch {
174
- exists = false;
175
- }
176
- if (!exists) {
177
- entries.push({ metadata, status: "stale", detail: "plist missing" });
178
- continue;
179
- }
180
- const label = launchdLabelForMetadata(metadata);
181
- const printed = await input.deps.runCommand("launchctl", ["print", `gui/${uid}/${label}`]);
182
- entries.push({
183
- metadata,
184
- status: printed.exitCode === 0 ? "active" : "stale",
185
- detail: printed.exitCode === 0 ? "loaded" : "plist present but not loaded",
186
- });
187
- }
188
- return entries;
189
- },
190
- async cancel(input) {
191
- const uid = resolveUid(input.deps);
192
- const label = launchdLabelForMetadata(input.metadata);
193
- const plistPath = input.metadata.unit_path ?? launchdPlistPathForId(input.metadata.id, input.deps.homeDir);
194
- let plistExisted = true;
195
- try {
196
- await fs.access(plistPath);
197
- }
198
- catch {
199
- plistExisted = false;
200
- }
201
- // bootout is best-effort: an already-unloaded label returns non-zero, which
202
- // is treated as stale-but-cancelable rather than a hard failure.
203
- const bootout = await input.deps.runCommand("launchctl", [
204
- "bootout",
205
- `gui/${uid}/${label}`,
206
- ]);
207
- if (plistExisted) {
208
- try {
209
- await fs.unlink(plistPath);
210
- }
211
- catch (error) {
212
- if (error.code !== "ENOENT") {
213
- return { ok: false, nativeRemoved: false, stale: false, error: String(error) };
214
- }
215
- }
216
- }
217
- const nativeRemoved = bootout.exitCode === 0;
218
- return {
219
- ok: true,
220
- nativeRemoved,
221
- stale: !plistExisted || !nativeRemoved,
222
- };
223
- },
224
- };
225
- }
@@ -1,250 +0,0 @@
1
- /**
2
- * Linux systemd-user backend (BAPI-327, primary Linux scheduler).
3
- *
4
- * Generates a `.service` + `.timer` pair under `~/.config/systemd/user/` and
5
- * installs them with `systemctl --user`. The timer fires once via
6
- * `OnCalendar=<run_at_iso>`.
7
- *
8
- * Drift policy: `Persistent=false` gives a real OS-level no-catch-up guarantee —
9
- * a run missed during downtime is dropped, not fired late on next boot/login.
10
- * (launchd and `at` cannot guarantee this; the cross-backend staleness guard is
11
- * the baked `--scheduled-at <T>` value checked by `/full-automation` in Phase C.)
12
- * Working directory is set by the service's `WorkingDirectory=` directive, never
13
- * via a shell `cd` in ExecStart and never a Claude `--cwd` flag.
14
- */
15
- import { promises as fs } from "node:fs";
16
- import { pathApiForPlatform } from "./types.js";
17
- import { bakedEnvFromCreateInput, bakedEnvEntries, systemdQuote } from "./escaping.js";
18
- /** Return the unit names for a NEW schedule id (neutral schedule-run naming). */
19
- export function systemdUnitNamesForId(id) {
20
- return {
21
- service: `bridge-gpt-schedule-run-${id}.service`,
22
- timer: `bridge-gpt-schedule-run-${id}.timer`,
23
- };
24
- }
25
- /** Return the unit file paths for a schedule id. */
26
- export function systemdUnitPathsForId(id, homeDir) {
27
- const pathApi = pathApiForPlatform("linux");
28
- const dir = pathApi.join(homeDir, ".config", "systemd", "user");
29
- const names = systemdUnitNamesForId(id);
30
- return {
31
- service: pathApi.join(dir, names.service),
32
- timer: pathApi.join(dir, names.timer),
33
- };
34
- }
35
- /**
36
- * Resolve unit names + paths for an EXISTING schedule, preferring the names baked
37
- * into the recorded unit paths so legacy `bridge-gpt-full-automation-<id>` units
38
- * remain list/cancel compatible after the neutral-naming change.
39
- */
40
- export function systemdUnitsForMetadata(metadata, homeDir) {
41
- const timerPath = metadata.unit_path && metadata.unit_path.endsWith(".timer") ? metadata.unit_path : undefined;
42
- const servicePath = (metadata.unit_paths ?? []).find((p) => p.endsWith(".service"));
43
- if (timerPath && servicePath) {
44
- const basename = (p) => p.split(/[\\/]/).pop();
45
- return {
46
- names: { service: basename(servicePath), timer: basename(timerPath) },
47
- paths: { service: servicePath, timer: timerPath },
48
- };
49
- }
50
- return {
51
- names: systemdUnitNamesForId(metadata.id),
52
- paths: systemdUnitPathsForId(metadata.id, homeDir),
53
- };
54
- }
55
- /** Render the systemd service unit. */
56
- export function renderSystemdService(input) {
57
- const env = bakedEnvEntries(bakedEnvFromCreateInput(input));
58
- const envLines = env
59
- .map(([key, value]) => `Environment=${key}=${systemdQuote(value)}`)
60
- .join("\n");
61
- // ExecStart runs the Node execution shim (trigger invocation); the shim spawns
62
- // the stored agent invocation and records run-history events.
63
- const execStart = [input.triggerInvocation.exe, ...input.triggerInvocation.args]
64
- .map((part) => systemdQuote(part))
65
- .join(" ");
66
- return [
67
- "[Unit]",
68
- `Description=Bridge GPT schedule-run one-shot (${input.id}: ${input.command})`,
69
- "",
70
- "[Service]",
71
- "Type=oneshot",
72
- envLines,
73
- `WorkingDirectory=${systemdQuote(input.repoPath)}`,
74
- `ExecStart=${execStart}`,
75
- `StandardOutput=append:${input.paths.stdoutPath}`,
76
- `StandardError=append:${input.paths.stderrPath}`,
77
- "",
78
- ].join("\n");
79
- }
80
- /**
81
- * Render an `OnCalendar` value systemd actually accepts. systemd's calendar
82
- * grammar is `YYYY-MM-DD HH:MM:SS` with an optional timezone token — it does NOT
83
- * accept the ISO `T` separator, fractional seconds, or a bare `Z`, so emitting
84
- * `run_at_iso` verbatim makes `systemd-analyze calendar` reject the unit and the
85
- * timer silently never fires. Render from the UTC fields (host-timezone
86
- * independent) and append the `UTC` token.
87
- */
88
- export function formatSystemdOnCalendar(runAtIso) {
89
- const d = new Date(runAtIso);
90
- const pad = (n) => String(n).padStart(2, "0");
91
- const date = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
92
- const time = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
93
- return `${date} ${time} UTC`;
94
- }
95
- /** Render the systemd timer unit. */
96
- export function renderSystemdTimer(input) {
97
- const names = systemdUnitNamesForId(input.id);
98
- return [
99
- "[Unit]",
100
- `Description=Bridge GPT schedule-run one-shot timer (${input.id}: ${input.command})`,
101
- "",
102
- "[Timer]",
103
- `OnCalendar=${formatSystemdOnCalendar(input.runAtIso)}`,
104
- // Drift policy: a missed run must NOT fire late on next boot.
105
- "Persistent=false",
106
- `Unit=${names.service}`,
107
- "",
108
- "[Install]",
109
- "WantedBy=timers.target",
110
- "",
111
- ].join("\n");
112
- }
113
- /** Whether `systemctl --user` is usable (used by isAvailable). */
114
- async function systemctlUserUsable(deps) {
115
- const probe = await deps.runCommand("systemctl", ["--user", "--version"]);
116
- return probe.exitCode === 0;
117
- }
118
- /** Create the Linux systemd-user backend. */
119
- export function createSystemdUserBackend() {
120
- return {
121
- name: "systemd-user",
122
- async isAvailable(deps) {
123
- if (deps.platform !== "linux")
124
- return false;
125
- return systemctlUserUsable(deps);
126
- },
127
- async create(input) {
128
- const paths = systemdUnitPathsForId(input.id, input.deps.homeDir);
129
- const names = systemdUnitNamesForId(input.id);
130
- const serviceContent = renderSystemdService(input);
131
- const timerContent = renderSystemdTimer(input);
132
- const artifacts = [
133
- { path: paths.service, content: serviceContent, kind: "systemd-service" },
134
- { path: paths.timer, content: timerContent, kind: "systemd-timer" },
135
- ];
136
- const unitPaths = [paths.service, paths.timer];
137
- if (input.dryRun) {
138
- return {
139
- ok: true,
140
- backend: "systemd-user",
141
- unitPath: paths.timer,
142
- unitPaths,
143
- backendJobId: names.timer,
144
- artifacts,
145
- };
146
- }
147
- const pathApi = pathApiForPlatform("linux");
148
- await fs.mkdir(pathApi.dirname(paths.service), { recursive: true });
149
- await fs.writeFile(paths.service, serviceContent, "utf-8");
150
- await fs.writeFile(paths.timer, timerContent, "utf-8");
151
- // Remove the orphaned unit files if a systemctl step fails.
152
- const cleanupUnitFiles = async () => {
153
- await fs.unlink(paths.timer).catch(() => undefined);
154
- await fs.unlink(paths.service).catch(() => undefined);
155
- };
156
- const reload = await input.deps.runCommand("systemctl", ["--user", "daemon-reload"]);
157
- if (reload.exitCode !== 0) {
158
- await cleanupUnitFiles();
159
- return {
160
- ok: false,
161
- backend: "systemd-user",
162
- unitPath: paths.timer,
163
- unitPaths,
164
- backendJobId: names.timer,
165
- artifacts,
166
- error: `systemctl --user daemon-reload failed: ${(reload.stderr || reload.stdout).trim()}`,
167
- };
168
- }
169
- const enable = await input.deps.runCommand("systemctl", [
170
- "--user",
171
- "enable",
172
- "--now",
173
- names.timer,
174
- ]);
175
- if (enable.exitCode !== 0) {
176
- await cleanupUnitFiles();
177
- return {
178
- ok: false,
179
- backend: "systemd-user",
180
- unitPath: paths.timer,
181
- unitPaths,
182
- backendJobId: names.timer,
183
- artifacts,
184
- error: `systemctl --user enable --now failed: ${(enable.stderr || enable.stdout).trim()}`,
185
- };
186
- }
187
- return {
188
- ok: true,
189
- backend: "systemd-user",
190
- unitPath: paths.timer,
191
- unitPaths,
192
- backendJobId: names.timer,
193
- artifacts,
194
- };
195
- },
196
- async list(input) {
197
- const entries = [];
198
- for (const metadata of input.recorded) {
199
- const { paths, names } = systemdUnitsForMetadata(metadata, input.deps.homeDir);
200
- let timerExists = true;
201
- try {
202
- await fs.access(paths.timer);
203
- }
204
- catch {
205
- timerExists = false;
206
- }
207
- if (!timerExists) {
208
- entries.push({ metadata, status: "stale", detail: "timer unit file missing" });
209
- continue;
210
- }
211
- const status = await input.deps.runCommand("systemctl", ["--user", "status", names.timer]);
212
- if (status.exitCode === 0) {
213
- entries.push({ metadata, status: "active", detail: "timer active" });
214
- }
215
- else if (status.exitCode === 3) {
216
- // exit 3 = unit known but inactive; still registered, treat as stale.
217
- entries.push({ metadata, status: "stale", detail: "timer inactive" });
218
- }
219
- else {
220
- entries.push({ metadata, status: "backend-unavailable", detail: "systemctl unavailable" });
221
- }
222
- }
223
- return entries;
224
- },
225
- async cancel(input) {
226
- const { paths, names } = systemdUnitsForMetadata(input.metadata, input.deps.homeDir);
227
- const disable = await input.deps.runCommand("systemctl", [
228
- "--user",
229
- "disable",
230
- "--now",
231
- names.timer,
232
- ]);
233
- let removedAny = false;
234
- for (const unitPath of [paths.timer, paths.service]) {
235
- try {
236
- await fs.unlink(unitPath);
237
- removedAny = true;
238
- }
239
- catch (error) {
240
- if (error.code !== "ENOENT") {
241
- return { ok: false, nativeRemoved: false, stale: false, error: String(error) };
242
- }
243
- }
244
- }
245
- await input.deps.runCommand("systemctl", ["--user", "daemon-reload"]);
246
- const nativeRemoved = disable.exitCode === 0 || removedAny;
247
- return { ok: true, nativeRemoved, stale: disable.exitCode !== 0 };
248
- },
249
- };
250
- }