@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,496 +1,562 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { getStateHome } from "./run-state.js";
7
- import {
8
- compactRecommendRunForStatus,
9
- getRecommendPipelineRunTool,
10
- prepareRecommendPipelineRunTool,
11
- startRecommendPipelineRunTool
12
- } from "./recommend-mcp.js";
13
-
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { getStateHome } from "./run-state.js";
7
+ import {
8
+ compactRecommendRunForStatus,
9
+ getRecommendPipelineRunTool,
10
+ prepareRecommendPipelineRunTool,
11
+ startRecommendPipelineRunTool
12
+ } from "./recommend-mcp.js";
13
+
14
14
  const SCHEDULE_WORKER_FLAG = "--schedule-worker";
15
15
  const SCHEDULE_ID_FLAG = "--schedule-id";
16
16
  const TERMINAL_SCHEDULE_STATES = new Set(["completed", "failed", "canceled"]);
17
17
  const TERMINAL_RUN_STATES = new Set(["completed", "failed", "canceled"]);
18
-
19
- let spawnProcessImpl = spawn;
20
-
21
- function normalizeText(value) {
22
- return String(value || "").replace(/\s+/g, " ").trim();
23
- }
24
-
18
+ const SCHEDULE_FORBIDDEN_DEBUG_FIELDS = Object.freeze([
19
+ "debug_test_mode",
20
+ "allow_debug_test_mode",
21
+ "debug_force_list_end_after_processed",
22
+ "debug_force_context_recovery_after_processed",
23
+ "debug_force_cdp_reconnect_after_processed"
24
+ ]);
25
+
26
+ let spawnProcessImpl = spawn;
27
+
28
+ function normalizeText(value) {
29
+ return String(value || "").replace(/\s+/g, " ").trim();
30
+ }
31
+
25
32
  function clonePlain(value, fallback = null) {
26
- try {
27
- return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
28
- } catch {
29
- return fallback;
30
- }
33
+ try {
34
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
35
+ } catch {
36
+ return fallback;
37
+ }
31
38
  }
32
39
 
33
- function nowIso() {
34
- return new Date().toISOString();
40
+ function hasOwn(source, key) {
41
+ return Boolean(source && Object.prototype.hasOwnProperty.call(source, key));
35
42
  }
36
43
 
37
- function sleep(ms) {
38
- return new Promise((resolve) => setTimeout(resolve, ms));
39
- }
40
-
41
- function parseNonNegativeNumber(raw, fallback = null) {
42
- if (raw === undefined || raw === null || raw === "") return fallback;
43
- const parsed = Number(raw);
44
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
45
- }
46
-
47
- function parseRunAt(args = {}) {
48
- const direct = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
49
- if (direct) {
50
- const timestamp = Date.parse(direct);
51
- if (!Number.isFinite(timestamp)) {
52
- return {
53
- ok: false,
54
- error: `Invalid schedule_run_at: ${direct}`
55
- };
56
- }
57
- return {
58
- ok: true,
59
- runAtMs: timestamp,
60
- source: "schedule_run_at"
61
- };
44
+ function collectScheduledRecommendDebugOptions(args = {}) {
45
+ const options = [];
46
+ for (const field of SCHEDULE_FORBIDDEN_DEBUG_FIELDS) {
47
+ if (hasOwn(args, field)) options.push(field);
62
48
  }
63
49
 
64
- const delaySeconds = parseNonNegativeNumber(args.schedule_delay_seconds ?? args.scheduleDelaySeconds, null);
65
- if (delaySeconds !== null) {
66
- return {
67
- ok: true,
68
- runAtMs: Date.now() + Math.round(delaySeconds * 1000),
69
- source: "schedule_delay_seconds"
70
- };
50
+ const screeningMode = normalizeText(args.screening_mode || args.screeningMode).toLowerCase();
51
+ if (["deterministic", "local", "local_scorer"].includes(screeningMode)) {
52
+ options.push(`screening_mode=${screeningMode}`);
71
53
  }
72
-
73
- const delayMinutes = parseNonNegativeNumber(args.schedule_delay_minutes ?? args.scheduleDelayMinutes, null);
74
- if (delayMinutes !== null) {
75
- return {
76
- ok: true,
77
- runAtMs: Date.now() + Math.round(delayMinutes * 60 * 1000),
78
- source: "schedule_delay_minutes"
79
- };
54
+ if (args.use_llm === false || args.useLlm === false) {
55
+ options.push("use_llm=false");
80
56
  }
81
-
82
- return {
83
- ok: false,
84
- error: "schedule_run_at or schedule_delay_minutes/schedule_delay_seconds is required"
85
- };
86
- }
87
-
88
- function safeIdPart(value) {
89
- return normalizeText(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
90
- }
91
-
92
- function createScheduleId(raw = "") {
93
- const requested = safeIdPart(raw);
94
- if (requested) return requested;
95
- const suffix = Math.random().toString(36).slice(2, 10);
96
- return `mcp_recommend_schedule_${Date.now().toString(36)}_${suffix}`;
97
- }
98
-
99
- function getSchedulesDir() {
100
- return path.join(getStateHome(), "schedules");
101
- }
102
-
103
- function getScheduleArtifacts(scheduleId) {
104
- const id = safeIdPart(scheduleId);
105
- if (!id) throw new Error("schedule_id is required");
106
- return {
107
- schedule_path: path.join(getSchedulesDir(), `${id}.json`),
108
- worker_stdout_path: path.join(getSchedulesDir(), `${id}.worker.stdout.log`),
109
- worker_stderr_path: path.join(getSchedulesDir(), `${id}.worker.stderr.log`)
110
- };
111
- }
112
-
113
- function writeJsonAtomic(filePath, payload) {
114
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
115
- const tempPath = `${filePath}.tmp`;
116
- fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
117
- fs.renameSync(tempPath, filePath);
118
- return payload;
119
- }
120
-
121
- function readJsonFile(filePath) {
122
- try {
123
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
124
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
125
- } catch {
126
- return null;
57
+ if (args.allow_card_only_screening === true || args.allowCardOnlyScreening === true) {
58
+ options.push("allow_card_only_screening");
127
59
  }
128
- }
129
-
130
- function readSchedule(scheduleId) {
131
- const artifacts = getScheduleArtifacts(scheduleId);
132
- return readJsonFile(artifacts.schedule_path);
133
- }
134
-
135
- function writeSchedule(scheduleId, patch) {
136
- const artifacts = getScheduleArtifacts(scheduleId);
137
- const current = readJsonFile(artifacts.schedule_path) || {};
138
- return writeJsonAtomic(artifacts.schedule_path, {
139
- ...current,
140
- ...patch,
141
- schedule_id: scheduleId,
142
- updated_at: nowIso()
143
- });
144
- }
145
-
146
- function compactScheduleForStatus(schedule) {
147
- if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
148
- const compact = clonePlain(schedule, schedule);
149
- if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
150
- if (compact.launch_payload?.run) {
151
- compact.launch_payload = {
152
- ...compact.launch_payload,
153
- run: compactRecommendRunForStatus(compact.launch_payload.run)
154
- };
60
+ const detailLimit = Number.parseInt(String(args.detail_limit ?? args.detailLimit ?? ""), 10);
61
+ if (Number.isFinite(detailLimit) && detailLimit === 0) options.push("detail_limit=0");
62
+ if (args.no_filter === true || args.noFilter === true) options.push("no_filter");
63
+ if (args.filter_enabled === false || args.filterEnabled === false) options.push("filter_enabled=false");
64
+ if (args.dry_run_post_action === true || args.dryRunPostAction === true) {
65
+ options.push("dry_run_post_action");
155
66
  }
156
- return compact;
157
- }
158
-
159
- function isProcessAlive(pid) {
160
- if (!Number.isInteger(pid) || pid <= 0) return false;
161
- try {
162
- process.kill(pid, 0);
163
- return true;
164
- } catch {
165
- return false;
67
+ if (args.dry_run === true || args.dryRun === true) options.push("dry_run");
68
+
69
+ const requestedPostAction = normalizeText(
70
+ args.overrides?.post_action
71
+ ?? args.confirmation?.post_action_value
72
+ ?? args.post_action
73
+ ?? args.postAction
74
+ ).toLowerCase();
75
+ if (
76
+ requestedPostAction
77
+ && requestedPostAction !== "none"
78
+ && (args.execute_post_action === false || args.executePostAction === false)
79
+ ) {
80
+ options.push("execute_post_action=false");
166
81
  }
82
+ return Array.from(new Set(options));
167
83
  }
168
-
169
- function stripScheduleArgs(args = {}) {
170
- const cloned = clonePlain(args, {});
171
- for (const key of [
172
- "schedule_id",
173
- "scheduleId",
174
- "schedule_run_at",
175
- "scheduleRunAt",
176
- "run_at",
177
- "runAt",
178
- "schedule_delay_seconds",
179
- "scheduleDelaySeconds",
180
- "schedule_delay_minutes",
181
- "scheduleDelayMinutes"
182
- ]) {
183
- delete cloned[key];
184
- }
185
- return cloned;
186
- }
187
-
188
- function buildFailedSchedulePayload(error, extra = {}) {
189
- return {
190
- status: "FAILED",
191
- schedule_created: false,
192
- error: {
193
- code: "RECOMMEND_SCHEDULE_FAILED",
194
- message: error?.message || String(error || "Unable to schedule recommend run"),
195
- retryable: true
196
- },
197
- ...extra
198
- };
199
- }
200
-
201
- function launchScheduleWorker(scheduleId) {
202
- const artifacts = getScheduleArtifacts(scheduleId);
203
- fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
204
- const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
205
- const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
206
- let child;
207
- try {
208
- child = spawnProcessImpl(process.execPath, [
209
- thisFilePath,
210
- SCHEDULE_WORKER_FLAG,
211
- SCHEDULE_ID_FLAG,
212
- scheduleId
213
- ], {
214
- cwd: process.cwd(),
215
- detached: true,
216
- stdio: ["ignore", stdoutFd, stderrFd],
217
- windowsHide: true,
218
- env: process.env
219
- });
220
- } finally {
221
- fs.closeSync(stdoutFd);
222
- fs.closeSync(stderrFd);
223
- }
224
- if (typeof child?.unref === "function") child.unref();
225
- return {
226
- pid: child.pid || null,
227
- stdoutPath: artifacts.worker_stdout_path,
228
- stderrPath: artifacts.worker_stderr_path
229
- };
230
- }
231
-
84
+
85
+ function nowIso() {
86
+ return new Date().toISOString();
87
+ }
88
+
89
+ function sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
+ }
92
+
93
+ function parseNonNegativeNumber(raw, fallback = null) {
94
+ if (raw === undefined || raw === null || raw === "") return fallback;
95
+ const parsed = Number(raw);
96
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
97
+ }
98
+
99
+ function parseRunAt(args = {}) {
100
+ const direct = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
101
+ if (direct) {
102
+ const timestamp = Date.parse(direct);
103
+ if (!Number.isFinite(timestamp)) {
104
+ return {
105
+ ok: false,
106
+ error: `Invalid schedule_run_at: ${direct}`
107
+ };
108
+ }
109
+ return {
110
+ ok: true,
111
+ runAtMs: timestamp,
112
+ source: "schedule_run_at"
113
+ };
114
+ }
115
+
116
+ const delaySeconds = parseNonNegativeNumber(args.schedule_delay_seconds ?? args.scheduleDelaySeconds, null);
117
+ if (delaySeconds !== null) {
118
+ return {
119
+ ok: true,
120
+ runAtMs: Date.now() + Math.round(delaySeconds * 1000),
121
+ source: "schedule_delay_seconds"
122
+ };
123
+ }
124
+
125
+ const delayMinutes = parseNonNegativeNumber(args.schedule_delay_minutes ?? args.scheduleDelayMinutes, null);
126
+ if (delayMinutes !== null) {
127
+ return {
128
+ ok: true,
129
+ runAtMs: Date.now() + Math.round(delayMinutes * 60 * 1000),
130
+ source: "schedule_delay_minutes"
131
+ };
132
+ }
133
+
134
+ return {
135
+ ok: false,
136
+ error: "schedule_run_at or schedule_delay_minutes/schedule_delay_seconds is required"
137
+ };
138
+ }
139
+
140
+ function safeIdPart(value) {
141
+ return normalizeText(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
142
+ }
143
+
144
+ function createScheduleId(raw = "") {
145
+ const requested = safeIdPart(raw);
146
+ if (requested) return requested;
147
+ const suffix = Math.random().toString(36).slice(2, 10);
148
+ return `mcp_recommend_schedule_${Date.now().toString(36)}_${suffix}`;
149
+ }
150
+
151
+ function getSchedulesDir() {
152
+ return path.join(getStateHome(), "schedules");
153
+ }
154
+
155
+ function getScheduleArtifacts(scheduleId) {
156
+ const id = safeIdPart(scheduleId);
157
+ if (!id) throw new Error("schedule_id is required");
158
+ return {
159
+ schedule_path: path.join(getSchedulesDir(), `${id}.json`),
160
+ worker_stdout_path: path.join(getSchedulesDir(), `${id}.worker.stdout.log`),
161
+ worker_stderr_path: path.join(getSchedulesDir(), `${id}.worker.stderr.log`)
162
+ };
163
+ }
164
+
165
+ function writeJsonAtomic(filePath, payload) {
166
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
167
+ const tempPath = `${filePath}.tmp`;
168
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
169
+ fs.renameSync(tempPath, filePath);
170
+ return payload;
171
+ }
172
+
173
+ function readJsonFile(filePath) {
174
+ try {
175
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
176
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function readSchedule(scheduleId) {
183
+ const artifacts = getScheduleArtifacts(scheduleId);
184
+ return readJsonFile(artifacts.schedule_path);
185
+ }
186
+
187
+ function writeSchedule(scheduleId, patch) {
188
+ const artifacts = getScheduleArtifacts(scheduleId);
189
+ const current = readJsonFile(artifacts.schedule_path) || {};
190
+ return writeJsonAtomic(artifacts.schedule_path, {
191
+ ...current,
192
+ ...patch,
193
+ schedule_id: scheduleId,
194
+ updated_at: nowIso()
195
+ });
196
+ }
197
+
198
+ function compactScheduleForStatus(schedule) {
199
+ if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) return schedule || null;
200
+ const compact = clonePlain(schedule, schedule);
201
+ if (compact.run) compact.run = compactRecommendRunForStatus(compact.run);
202
+ if (compact.launch_payload?.run) {
203
+ compact.launch_payload = {
204
+ ...compact.launch_payload,
205
+ run: compactRecommendRunForStatus(compact.launch_payload.run)
206
+ };
207
+ }
208
+ return compact;
209
+ }
210
+
211
+ function isProcessAlive(pid) {
212
+ if (!Number.isInteger(pid) || pid <= 0) return false;
213
+ try {
214
+ process.kill(pid, 0);
215
+ return true;
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ function stripScheduleArgs(args = {}) {
222
+ const cloned = clonePlain(args, {});
223
+ for (const key of [
224
+ "schedule_id",
225
+ "scheduleId",
226
+ "schedule_run_at",
227
+ "scheduleRunAt",
228
+ "run_at",
229
+ "runAt",
230
+ "schedule_delay_seconds",
231
+ "scheduleDelaySeconds",
232
+ "schedule_delay_minutes",
233
+ "scheduleDelayMinutes"
234
+ ]) {
235
+ delete cloned[key];
236
+ }
237
+ return cloned;
238
+ }
239
+
240
+ function buildFailedSchedulePayload(error, extra = {}) {
241
+ return {
242
+ status: "FAILED",
243
+ schedule_created: false,
244
+ error: {
245
+ code: "RECOMMEND_SCHEDULE_FAILED",
246
+ message: error?.message || String(error || "Unable to schedule recommend run"),
247
+ retryable: true
248
+ },
249
+ ...extra
250
+ };
251
+ }
252
+
253
+ function launchScheduleWorker(scheduleId) {
254
+ const artifacts = getScheduleArtifacts(scheduleId);
255
+ fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
256
+ const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
257
+ const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
258
+ let child;
259
+ try {
260
+ child = spawnProcessImpl(process.execPath, [
261
+ thisFilePath,
262
+ SCHEDULE_WORKER_FLAG,
263
+ SCHEDULE_ID_FLAG,
264
+ scheduleId
265
+ ], {
266
+ cwd: process.cwd(),
267
+ detached: true,
268
+ stdio: ["ignore", stdoutFd, stderrFd],
269
+ windowsHide: true,
270
+ env: process.env
271
+ });
272
+ } finally {
273
+ fs.closeSync(stdoutFd);
274
+ fs.closeSync(stderrFd);
275
+ }
276
+ if (typeof child?.unref === "function") child.unref();
277
+ return {
278
+ pid: child.pid || null,
279
+ stdoutPath: artifacts.worker_stdout_path,
280
+ stderrPath: artifacts.worker_stderr_path
281
+ };
282
+ }
283
+
232
284
  export async function scheduleRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
233
285
  const runArgs = stripScheduleArgs(args);
234
- const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args: runArgs });
235
- if (prepared.status !== "READY" || prepared.cron_ready !== true) {
236
- return {
237
- ...prepared,
238
- status: prepared.status || "FAILED",
239
- schedule_created: false,
240
- cron_ready: false,
241
- message: "Recommend schedule was not created because the run payload is not READY."
242
- };
243
- }
244
-
245
- const due = parseRunAt(args);
246
- if (!due.ok) {
286
+ const forbiddenDebugOptions = collectScheduledRecommendDebugOptions(runArgs);
287
+ if (forbiddenDebugOptions.length) {
247
288
  return {
248
289
  status: "FAILED",
249
290
  schedule_created: false,
250
- cron_ready: true,
291
+ cron_ready: false,
251
292
  error: {
252
- code: "INVALID_SCHEDULE_TIME",
253
- message: due.error,
293
+ code: "RECOMMEND_SCHEDULE_DEBUG_OPTIONS_FORBIDDEN",
294
+ message: `Recommend schedules cannot contain diagnostic-only run options: ${forbiddenDebugOptions.join(", ")}`,
254
295
  retryable: false
255
296
  },
256
- prepare: prepared
257
- };
258
- }
259
-
260
- const scheduleId = createScheduleId(args.schedule_id || args.scheduleId);
261
- const artifacts = getScheduleArtifacts(scheduleId);
262
- const runAtIso = new Date(due.runAtMs).toISOString();
263
- const createdAt = nowIso();
264
- try {
265
- writeJsonAtomic(artifacts.schedule_path, {
266
- schedule_id: scheduleId,
267
- state: "scheduled",
268
- status: "scheduled",
269
- created_at: createdAt,
270
- updated_at: createdAt,
271
- run_at: runAtIso,
272
- run_at_ms: due.runAtMs,
273
- time_source: due.source,
274
- workspace_root: path.resolve(workspaceRoot || process.cwd()),
275
- args: runArgs,
276
- prepare: prepared,
277
- worker_stdout_path: artifacts.worker_stdout_path,
278
- worker_stderr_path: artifacts.worker_stderr_path,
279
- pid: null,
280
- run_id: null,
281
- run: null,
282
- error: null
283
- });
284
- } catch (error) {
285
- return buildFailedSchedulePayload(error, { prepare: prepared });
286
- }
287
-
288
- let worker;
289
- try {
290
- worker = launchScheduleWorker(scheduleId);
291
- } catch (error) {
292
- writeSchedule(scheduleId, {
293
- state: "failed",
294
- status: "failed",
295
- error: {
296
- code: "SCHEDULE_WORKER_LAUNCH_FAILED",
297
- message: error?.message || String(error || "Unable to launch schedule worker"),
298
- retryable: true
299
- }
300
- });
301
- return buildFailedSchedulePayload(error, {
302
- schedule_created: true,
303
- schedule_id: scheduleId,
304
- schedule: readSchedule(scheduleId),
305
- prepare: prepared
306
- });
307
- }
308
-
309
- const schedule = writeSchedule(scheduleId, {
310
- state: "scheduled",
311
- status: "scheduled",
312
- pid: worker.pid,
313
- worker_stdout_path: worker.stdoutPath,
314
- worker_stderr_path: worker.stderrPath
315
- });
316
-
317
- return {
318
- status: "SCHEDULED",
319
- schedule_created: true,
320
- cron_ready: true,
321
- schedule_id: scheduleId,
322
- run_at: runAtIso,
323
- run_at_ms: due.runAtMs,
324
- worker_pid: worker.pid,
325
- worker_stdout_path: worker.stdoutPath,
326
- worker_stderr_path: worker.stderrPath,
327
- schedule,
328
- prepare: prepared,
329
- message: "Recommend run schedule created. The package-owned detached scheduler will start the prepared payload at run_at."
330
- };
331
- }
332
-
333
- export function getRecommendScheduledRunTool({ args = {} } = {}) {
334
- const scheduleId = safeIdPart(args.schedule_id || args.scheduleId);
335
- if (!scheduleId) {
336
- return {
337
- status: "FAILED",
338
- error: {
339
- code: "INVALID_SCHEDULE_ID",
340
- message: "schedule_id is required",
341
- retryable: false
342
- }
343
- };
344
- }
345
- const schedule = readSchedule(scheduleId);
346
- if (!schedule) {
347
- return {
348
- status: "FAILED",
349
- error: {
350
- code: "SCHEDULE_NOT_FOUND",
351
- message: `schedule_id=${scheduleId} not found`,
352
- retryable: false
353
- }
297
+ forbidden_debug_options: forbiddenDebugOptions
354
298
  };
355
299
  }
356
- let next = schedule;
357
- if (!TERMINAL_SCHEDULE_STATES.has(normalizeText(schedule.state || schedule.status)) && schedule.pid && !isProcessAlive(schedule.pid)) {
358
- next = writeSchedule(scheduleId, {
359
- state: "failed",
360
- status: "failed",
361
- completed_at: nowIso(),
362
- error: {
363
- code: "SCHEDULE_WORKER_EXITED",
364
- message: `Scheduled worker process exited before reaching a terminal state (pid=${schedule.pid}).`,
365
- retryable: true
366
- }
367
- });
368
- }
369
- return {
370
- status: "OK",
371
- schedule_id: scheduleId,
372
- schedule: compactScheduleForStatus(next)
373
- };
374
- }
375
-
376
- export async function runScheduledRecommendWorker({ scheduleId }) {
377
- const normalizedScheduleId = safeIdPart(scheduleId);
378
- if (!normalizedScheduleId) return { ok: false, error: "schedule_id is required" };
379
- let schedule = readSchedule(normalizedScheduleId);
380
- if (!schedule) return { ok: false, error: `schedule_id=${normalizedScheduleId} not found` };
381
- const runAtMs = Number(schedule.run_at_ms);
382
- if (!Number.isFinite(runAtMs)) {
383
- writeSchedule(normalizedScheduleId, {
384
- state: "failed",
385
- status: "failed",
386
- completed_at: nowIso(),
387
- error: {
388
- code: "INVALID_SCHEDULE_STATE",
389
- message: "schedule is missing a valid run_at_ms",
390
- retryable: false
391
- }
392
- });
393
- return { ok: false, error: "schedule is missing a valid run_at_ms" };
394
- }
395
-
396
- schedule = writeSchedule(normalizedScheduleId, {
397
- state: "waiting",
398
- status: "waiting",
399
- pid: process.pid,
400
- worker_started_at: nowIso()
401
- });
402
-
403
- while (Date.now() < runAtMs) {
404
- await sleep(Math.min(30_000, Math.max(50, runAtMs - Date.now())));
405
- const latest = readSchedule(normalizedScheduleId);
406
- if (normalizeText(latest?.state) === "canceled") return { ok: true, canceled: true };
407
- }
408
-
409
- writeSchedule(normalizedScheduleId, {
410
- state: "launching",
411
- status: "launching",
412
- launch_started_at: nowIso()
413
- });
414
-
415
- const started = await startRecommendPipelineRunTool({
416
- workspaceRoot: schedule.workspace_root,
417
- args: clonePlain(schedule.args, {})
418
- });
419
- if (started.status !== "ACCEPTED") {
420
- writeSchedule(normalizedScheduleId, {
421
- state: "failed",
422
- status: "failed",
423
- completed_at: nowIso(),
424
- launch_payload: started,
425
- error: started.error || {
426
- code: "RECOMMEND_START_NOT_ACCEPTED",
427
- message: started.status || "start_recommend_pipeline_run did not return ACCEPTED",
428
- retryable: true
429
- }
430
- });
431
- return { ok: false, error: started.error?.message || started.status || "not accepted" };
432
- }
433
-
434
- writeSchedule(normalizedScheduleId, {
435
- state: "running",
436
- status: "running",
437
- run_id: started.run_id,
438
- run: started.run || null,
439
- launch_payload: started,
440
- launched_at: nowIso()
441
- });
442
-
443
- while (true) {
444
- const payload = getRecommendPipelineRunTool({ args: { run_id: started.run_id } });
445
- const runState = normalizeText(payload?.run?.state || payload?.run?.status);
446
- writeSchedule(normalizedScheduleId, {
447
- state: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
448
- status: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
449
- run_id: started.run_id,
450
- run: payload?.run || null,
451
- last_poll_at: nowIso(),
452
- completed_at: runState && TERMINAL_RUN_STATES.has(runState) ? nowIso() : undefined,
453
- error: runState === "failed" ? (payload?.run?.error || payload?.error || null) : null
454
- });
455
- if (TERMINAL_RUN_STATES.has(runState)) break;
456
- await sleep(1000);
457
- }
458
- return { ok: true, run_id: started.run_id };
459
- }
460
-
461
- export function __setRecommendSchedulerSpawnForTests(nextImpl) {
462
- spawnProcessImpl = typeof nextImpl === "function" ? nextImpl : spawn;
463
- }
464
-
465
- function parseScheduleWorkerOptions(argv = process.argv.slice(2)) {
466
- if (!Array.isArray(argv) || !argv.includes(SCHEDULE_WORKER_FLAG)) return null;
467
- const idIndex = argv.indexOf(SCHEDULE_ID_FLAG);
468
- return {
469
- scheduleId: idIndex >= 0 ? normalizeText(argv[idIndex + 1]) : ""
470
- };
471
- }
472
-
473
- const thisFilePath = fileURLToPath(import.meta.url);
474
- if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
475
- const options = parseScheduleWorkerOptions(process.argv.slice(2));
476
- if (options) {
477
- runScheduledRecommendWorker(options).then((result) => {
478
- if (!result?.ok) process.exitCode = 1;
479
- }).catch((error) => {
480
- try {
481
- writeSchedule(options.scheduleId, {
482
- state: "failed",
483
- status: "failed",
484
- completed_at: nowIso(),
485
- error: {
486
- code: "SCHEDULE_WORKER_UNHANDLED_ERROR",
487
- message: error?.message || String(error || "schedule worker failed"),
488
- retryable: true
489
- }
490
- });
491
- } catch {}
492
- console.error("[boss-recommend-mcp] scheduled recommend worker failed", error);
493
- process.exitCode = 1;
494
- });
495
- }
496
- }
300
+ const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args: runArgs });
301
+ if (prepared.status !== "READY" || prepared.cron_ready !== true) {
302
+ return {
303
+ ...prepared,
304
+ status: prepared.status || "FAILED",
305
+ schedule_created: false,
306
+ cron_ready: false,
307
+ message: "Recommend schedule was not created because the run payload is not READY."
308
+ };
309
+ }
310
+
311
+ const due = parseRunAt(args);
312
+ if (!due.ok) {
313
+ return {
314
+ status: "FAILED",
315
+ schedule_created: false,
316
+ cron_ready: true,
317
+ error: {
318
+ code: "INVALID_SCHEDULE_TIME",
319
+ message: due.error,
320
+ retryable: false
321
+ },
322
+ prepare: prepared
323
+ };
324
+ }
325
+
326
+ const scheduleId = createScheduleId(args.schedule_id || args.scheduleId);
327
+ const artifacts = getScheduleArtifacts(scheduleId);
328
+ const runAtIso = new Date(due.runAtMs).toISOString();
329
+ const createdAt = nowIso();
330
+ try {
331
+ writeJsonAtomic(artifacts.schedule_path, {
332
+ schedule_id: scheduleId,
333
+ state: "scheduled",
334
+ status: "scheduled",
335
+ created_at: createdAt,
336
+ updated_at: createdAt,
337
+ run_at: runAtIso,
338
+ run_at_ms: due.runAtMs,
339
+ time_source: due.source,
340
+ workspace_root: path.resolve(workspaceRoot || process.cwd()),
341
+ args: runArgs,
342
+ prepare: prepared,
343
+ worker_stdout_path: artifacts.worker_stdout_path,
344
+ worker_stderr_path: artifacts.worker_stderr_path,
345
+ pid: null,
346
+ run_id: null,
347
+ run: null,
348
+ error: null
349
+ });
350
+ } catch (error) {
351
+ return buildFailedSchedulePayload(error, { prepare: prepared });
352
+ }
353
+
354
+ let worker;
355
+ try {
356
+ worker = launchScheduleWorker(scheduleId);
357
+ } catch (error) {
358
+ writeSchedule(scheduleId, {
359
+ state: "failed",
360
+ status: "failed",
361
+ error: {
362
+ code: "SCHEDULE_WORKER_LAUNCH_FAILED",
363
+ message: error?.message || String(error || "Unable to launch schedule worker"),
364
+ retryable: true
365
+ }
366
+ });
367
+ return buildFailedSchedulePayload(error, {
368
+ schedule_created: true,
369
+ schedule_id: scheduleId,
370
+ schedule: readSchedule(scheduleId),
371
+ prepare: prepared
372
+ });
373
+ }
374
+
375
+ const schedule = writeSchedule(scheduleId, {
376
+ state: "scheduled",
377
+ status: "scheduled",
378
+ pid: worker.pid,
379
+ worker_stdout_path: worker.stdoutPath,
380
+ worker_stderr_path: worker.stderrPath
381
+ });
382
+
383
+ return {
384
+ status: "SCHEDULED",
385
+ schedule_created: true,
386
+ cron_ready: true,
387
+ schedule_id: scheduleId,
388
+ run_at: runAtIso,
389
+ run_at_ms: due.runAtMs,
390
+ worker_pid: worker.pid,
391
+ worker_stdout_path: worker.stdoutPath,
392
+ worker_stderr_path: worker.stderrPath,
393
+ schedule,
394
+ prepare: prepared,
395
+ message: "Recommend run schedule created. The package-owned detached scheduler will start the prepared payload at run_at."
396
+ };
397
+ }
398
+
399
+ export function getRecommendScheduledRunTool({ args = {} } = {}) {
400
+ const scheduleId = safeIdPart(args.schedule_id || args.scheduleId);
401
+ if (!scheduleId) {
402
+ return {
403
+ status: "FAILED",
404
+ error: {
405
+ code: "INVALID_SCHEDULE_ID",
406
+ message: "schedule_id is required",
407
+ retryable: false
408
+ }
409
+ };
410
+ }
411
+ const schedule = readSchedule(scheduleId);
412
+ if (!schedule) {
413
+ return {
414
+ status: "FAILED",
415
+ error: {
416
+ code: "SCHEDULE_NOT_FOUND",
417
+ message: `schedule_id=${scheduleId} not found`,
418
+ retryable: false
419
+ }
420
+ };
421
+ }
422
+ let next = schedule;
423
+ if (!TERMINAL_SCHEDULE_STATES.has(normalizeText(schedule.state || schedule.status)) && schedule.pid && !isProcessAlive(schedule.pid)) {
424
+ next = writeSchedule(scheduleId, {
425
+ state: "failed",
426
+ status: "failed",
427
+ completed_at: nowIso(),
428
+ error: {
429
+ code: "SCHEDULE_WORKER_EXITED",
430
+ message: `Scheduled worker process exited before reaching a terminal state (pid=${schedule.pid}).`,
431
+ retryable: true
432
+ }
433
+ });
434
+ }
435
+ return {
436
+ status: "OK",
437
+ schedule_id: scheduleId,
438
+ schedule: compactScheduleForStatus(next)
439
+ };
440
+ }
441
+
442
+ export async function runScheduledRecommendWorker({ scheduleId }) {
443
+ const normalizedScheduleId = safeIdPart(scheduleId);
444
+ if (!normalizedScheduleId) return { ok: false, error: "schedule_id is required" };
445
+ let schedule = readSchedule(normalizedScheduleId);
446
+ if (!schedule) return { ok: false, error: `schedule_id=${normalizedScheduleId} not found` };
447
+ const runAtMs = Number(schedule.run_at_ms);
448
+ if (!Number.isFinite(runAtMs)) {
449
+ writeSchedule(normalizedScheduleId, {
450
+ state: "failed",
451
+ status: "failed",
452
+ completed_at: nowIso(),
453
+ error: {
454
+ code: "INVALID_SCHEDULE_STATE",
455
+ message: "schedule is missing a valid run_at_ms",
456
+ retryable: false
457
+ }
458
+ });
459
+ return { ok: false, error: "schedule is missing a valid run_at_ms" };
460
+ }
461
+
462
+ schedule = writeSchedule(normalizedScheduleId, {
463
+ state: "waiting",
464
+ status: "waiting",
465
+ pid: process.pid,
466
+ worker_started_at: nowIso()
467
+ });
468
+
469
+ while (Date.now() < runAtMs) {
470
+ await sleep(Math.min(30_000, Math.max(50, runAtMs - Date.now())));
471
+ const latest = readSchedule(normalizedScheduleId);
472
+ if (normalizeText(latest?.state) === "canceled") return { ok: true, canceled: true };
473
+ }
474
+
475
+ writeSchedule(normalizedScheduleId, {
476
+ state: "launching",
477
+ status: "launching",
478
+ launch_started_at: nowIso()
479
+ });
480
+
481
+ const started = await startRecommendPipelineRunTool({
482
+ workspaceRoot: schedule.workspace_root,
483
+ args: clonePlain(schedule.args, {})
484
+ });
485
+ if (started.status !== "ACCEPTED") {
486
+ writeSchedule(normalizedScheduleId, {
487
+ state: "failed",
488
+ status: "failed",
489
+ completed_at: nowIso(),
490
+ launch_payload: started,
491
+ error: started.error || {
492
+ code: "RECOMMEND_START_NOT_ACCEPTED",
493
+ message: started.status || "start_recommend_pipeline_run did not return ACCEPTED",
494
+ retryable: true
495
+ }
496
+ });
497
+ return { ok: false, error: started.error?.message || started.status || "not accepted" };
498
+ }
499
+
500
+ writeSchedule(normalizedScheduleId, {
501
+ state: "running",
502
+ status: "running",
503
+ run_id: started.run_id,
504
+ run: started.run || null,
505
+ launch_payload: started,
506
+ launched_at: nowIso()
507
+ });
508
+
509
+ while (true) {
510
+ const payload = getRecommendPipelineRunTool({ args: { run_id: started.run_id } });
511
+ const runState = normalizeText(payload?.run?.state || payload?.run?.status);
512
+ writeSchedule(normalizedScheduleId, {
513
+ state: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
514
+ status: runState && TERMINAL_RUN_STATES.has(runState) ? runState : "running",
515
+ run_id: started.run_id,
516
+ run: payload?.run || null,
517
+ last_poll_at: nowIso(),
518
+ completed_at: runState && TERMINAL_RUN_STATES.has(runState) ? nowIso() : undefined,
519
+ error: runState === "failed" ? (payload?.run?.error || payload?.error || null) : null
520
+ });
521
+ if (TERMINAL_RUN_STATES.has(runState)) break;
522
+ await sleep(1000);
523
+ }
524
+ return { ok: true, run_id: started.run_id };
525
+ }
526
+
527
+ export function __setRecommendSchedulerSpawnForTests(nextImpl) {
528
+ spawnProcessImpl = typeof nextImpl === "function" ? nextImpl : spawn;
529
+ }
530
+
531
+ function parseScheduleWorkerOptions(argv = process.argv.slice(2)) {
532
+ if (!Array.isArray(argv) || !argv.includes(SCHEDULE_WORKER_FLAG)) return null;
533
+ const idIndex = argv.indexOf(SCHEDULE_ID_FLAG);
534
+ return {
535
+ scheduleId: idIndex >= 0 ? normalizeText(argv[idIndex + 1]) : ""
536
+ };
537
+ }
538
+
539
+ const thisFilePath = fileURLToPath(import.meta.url);
540
+ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
541
+ const options = parseScheduleWorkerOptions(process.argv.slice(2));
542
+ if (options) {
543
+ runScheduledRecommendWorker(options).then((result) => {
544
+ if (!result?.ok) process.exitCode = 1;
545
+ }).catch((error) => {
546
+ try {
547
+ writeSchedule(options.scheduleId, {
548
+ state: "failed",
549
+ status: "failed",
550
+ completed_at: nowIso(),
551
+ error: {
552
+ code: "SCHEDULE_WORKER_UNHANDLED_ERROR",
553
+ message: error?.message || String(error || "schedule worker failed"),
554
+ retryable: true
555
+ }
556
+ });
557
+ } catch {}
558
+ console.error("[boss-recommend-mcp] scheduled recommend worker failed", error);
559
+ process.exitCode = 1;
560
+ });
561
+ }
562
+ }