@minionry/minion 0.7.36 → 0.7.39

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 (120) hide show
  1. package/bin/commands/minions.js +19 -0
  2. package/bin/minion.js +33 -4
  3. package/dist/server/cli/headless/claude-invoker-process.js +23 -7
  4. package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
  5. package/dist/server/cli/headless/haiku-assessments.js +2 -1
  6. package/dist/server/cli/headless/mcp-config.js +2 -1
  7. package/dist/server/engines/claude/claude-command.js +1 -0
  8. package/dist/server/engines/factory.js +2 -0
  9. package/dist/server/engines/hermes/HermesEngine.js +112 -219
  10. package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
  11. package/dist/server/index.js +10 -4
  12. package/dist/server/mcp/bouncer-integration.js +3 -2
  13. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
  14. package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
  15. package/dist/server/mcp/classifier/telemetry.js +0 -0
  16. package/dist/server/mcp/permission-channels.js +176 -0
  17. package/dist/server/mcp/security-analysis.js +9 -1
  18. package/dist/server/mcp/security-audit.js +22 -2
  19. package/dist/server/mcp/security-patterns.js +73 -1
  20. package/dist/server/mcp/server.js +80 -50
  21. package/dist/server/routes/internal.js +6 -1
  22. package/dist/server/routes/notifications.js +6 -18
  23. package/dist/server/server-setup.js +24 -48
  24. package/dist/server/services/analytics.js +4 -2
  25. package/dist/server/services/browser/host-mock.js +6 -1
  26. package/dist/server/services/browser/host.js +27 -3
  27. package/dist/server/services/browser/reasoning-agent.js +3 -1
  28. package/dist/server/services/chain/chain-engine.js +494 -0
  29. package/dist/server/services/chain/chain-store.js +542 -0
  30. package/dist/server/services/git/haiku.js +2 -2
  31. package/dist/server/services/plan/agents/check-injection.md +43 -2
  32. package/dist/server/services/plan/agents/review-code.md +62 -3
  33. package/dist/server/services/plan/agents/review-quality.md +52 -2
  34. package/dist/server/services/plan/board-export.js +4 -0
  35. package/dist/server/services/plan/composer-prompt.js +27 -8
  36. package/dist/server/services/plan/composer.js +10 -4
  37. package/dist/server/services/plan/config-installer.js +4 -83
  38. package/dist/server/services/plan/executor.js +155 -73
  39. package/dist/server/services/plan/issue-effort.js +9 -21
  40. package/dist/server/services/plan/issue-prompt-builder.js +72 -9
  41. package/dist/server/services/plan/parser-core.js +6 -1
  42. package/dist/server/services/plan/quality-delta.js +6 -24
  43. package/dist/server/services/plan/readiness-planner.js +12 -4
  44. package/dist/server/services/plan/record-file.js +52 -0
  45. package/dist/server/services/plan/review-approval.js +64 -0
  46. package/dist/server/services/plan/review-gate.js +186 -77
  47. package/dist/server/services/plan/review-outcome.js +146 -0
  48. package/dist/server/services/plan/review-report.js +28 -1
  49. package/dist/server/services/plan/review-target.js +57 -0
  50. package/dist/server/services/plan/state-reconciler.js +3 -3
  51. package/dist/server/services/plan/template-diff.js +2 -0
  52. package/dist/server/services/plan/template-instantiator.js +4 -0
  53. package/dist/server/services/plan/watcher.js +11 -1
  54. package/dist/server/services/platform-reconnect.js +7 -0
  55. package/dist/server/services/platform-token-lifecycle.js +2 -1
  56. package/dist/server/services/platform.js +293 -50
  57. package/dist/server/services/relay-http-client.js +107 -0
  58. package/dist/server/services/schedule/agent-schedule-store.js +69 -8
  59. package/dist/server/services/schedule/agent-scheduler.js +48 -7
  60. package/dist/server/services/schedule/daily-timing.js +52 -0
  61. package/dist/server/services/schedule/prompt-file.js +55 -0
  62. package/dist/server/services/schedule/run-bootstrap.js +20 -5
  63. package/dist/server/services/schedule/schedule-store.js +54 -4
  64. package/dist/server/services/schedule/schedule-wire.js +17 -0
  65. package/dist/server/services/schedule/scheduler.js +43 -9
  66. package/dist/server/services/sdk/agent-schedule.js +294 -0
  67. package/dist/server/services/sdk/agents.js +95 -13
  68. package/dist/server/services/sdk/app-tools.js +44 -7
  69. package/dist/server/services/sdk/atomic-write.js +75 -0
  70. package/dist/server/services/sdk/browser.js +5 -1
  71. package/dist/server/services/sdk/composer.js +2 -0
  72. package/dist/server/services/sdk/file-change-notify.js +60 -13
  73. package/dist/server/services/sdk/files-app.js +12 -7
  74. package/dist/server/services/sdk/files-search.js +14 -0
  75. package/dist/server/services/sdk/files-transfer.js +1 -1
  76. package/dist/server/services/sdk/files-watch.js +721 -0
  77. package/dist/server/services/sdk/files-workspace.js +37 -11
  78. package/dist/server/services/sdk/inference-claude.js +4 -2
  79. package/dist/server/services/sdk/inference.js +16 -2
  80. package/dist/server/services/sdk/pm-chain.js +0 -0
  81. package/dist/server/services/sdk/pm-schedule.js +72 -15
  82. package/dist/server/services/sdk/pm.js +22 -0
  83. package/dist/server/services/sdk/rate-limits.js +32 -0
  84. package/dist/server/services/sdk/registry.js +40 -2
  85. package/dist/server/services/sdk/terminal.js +1 -1
  86. package/dist/server/services/sentry.js +5 -1
  87. package/dist/server/services/terminal/pty-manager.js +26 -11
  88. package/dist/server/services/terminal/pty-utils.js +12 -1
  89. package/dist/server/services/terminal/terminal-modes.js +87 -0
  90. package/dist/server/services/timeline/assembler.js +2 -2
  91. package/dist/server/services/timeline/index.js +1 -1
  92. package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
  93. package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
  94. package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
  95. package/dist/server/services/websocket/browser-handlers.js +5 -2
  96. package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
  97. package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
  98. package/dist/server/services/websocket/file-transfer-http.js +12 -6
  99. package/dist/server/services/websocket/handler.js +185 -33
  100. package/dist/server/services/websocket/msg-id-tracker.js +94 -19
  101. package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
  102. package/dist/server/services/websocket/plan-handlers.js +10 -5
  103. package/dist/server/services/websocket/plan-helpers.js +23 -0
  104. package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
  105. package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
  106. package/dist/server/services/websocket/schedule-handlers.js +8 -2
  107. package/dist/server/services/websocket/sdk-agent-host.js +5 -1
  108. package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
  109. package/dist/server/services/websocket/sdk-browser-host.js +11 -4
  110. package/dist/server/services/websocket/sdk-handlers.js +6 -1
  111. package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
  112. package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
  113. package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
  114. package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
  115. package/dist/server/services/websocket/settings-handlers.js +6 -18
  116. package/dist/server/services/websocket/tab-broadcast.js +5 -1
  117. package/dist/server/services/websocket/terminal-handlers.js +51 -6
  118. package/dist/server/services/websocket/types.js +6 -5
  119. package/dist/server/services/websocket/viewer-interest.js +30 -0
  120. package/package.json +6 -6
@@ -6,7 +6,7 @@ import { resolvePmDir } from '../plan/parser.js';
6
6
  import { wireExecutorEvents } from '../websocket/plan-execution-handlers.js';
7
7
  import { schedulerCache } from '../websocket/plan-helpers.js';
8
8
  import { bootstrapRun } from './run-bootstrap.js';
9
- import { ScheduleStore } from './schedule-store.js';
9
+ import { computeNextFireAt, exceedsMaxLateness, isRecurringTiming, SCHEDULE_MISSED_LATENESS_ERROR, ScheduleStore, } from './schedule-store.js';
10
10
  export const SCHEDULER_TICK_MS = 30_000;
11
11
  export const SCHEDULE_ONCE_GRACE_MS = 15 * 60_000;
12
12
  export const CONTINUOUS_MIN_SPACING_MS = 60_000;
@@ -88,6 +88,25 @@ export class SchedulerEngine {
88
88
  this.runs.reset();
89
89
  this.store = null;
90
90
  }
91
+ fireNow(scheduleId) {
92
+ const store = this.store;
93
+ if (!store)
94
+ return { kind: 'not-found' };
95
+ const before = store.get(scheduleId);
96
+ if (!before)
97
+ return { kind: 'not-found' };
98
+ if (!before.enabled)
99
+ return { kind: 'disabled' };
100
+ const plannedNextFireAt = before.nextFireAt;
101
+ if (this.runs.has(scheduleId)) {
102
+ this.recordSkippedOverlap(before, Date.now());
103
+ }
104
+ else {
105
+ this.fire(before, Date.now());
106
+ }
107
+ store.restoreNextFireAt(scheduleId, plannedNextFireAt);
108
+ return { kind: 'ok', record: store.get(scheduleId) };
109
+ }
91
110
  tick() {
92
111
  const store = this.store;
93
112
  if (!store)
@@ -104,6 +123,10 @@ export class SchedulerEngine {
104
123
  }
105
124
  if (!this.isDue(record, now))
106
125
  continue;
126
+ if (exceedsMaxLateness(record, now)) {
127
+ this.recordMissedLate(record, now);
128
+ continue;
129
+ }
107
130
  this.fire(record, now);
108
131
  }
109
132
  }
@@ -132,8 +155,8 @@ export class SchedulerEngine {
132
155
  if (record.timing.kind === 'once') {
133
156
  this.recoverOnce(record, now);
134
157
  }
135
- else if (record.timing.kind === 'interval') {
136
- this.recoverInterval(record, now);
158
+ else if (isRecurringTiming(record.timing)) {
159
+ this.recoverRecurring(record, now);
137
160
  }
138
161
  }
139
162
  }
@@ -154,10 +177,14 @@ export class SchedulerEngine {
154
177
  store.setEnabled(record.id, false);
155
178
  this.broadcastSchedules();
156
179
  }
157
- recoverInterval(record, now) {
180
+ recoverRecurring(record, now) {
158
181
  const next = record.nextFireAt ? parseEpoch(record.nextFireAt) : Number.NaN;
159
182
  if (Number.isFinite(next) && next > now)
160
183
  return;
184
+ if (exceedsMaxLateness(record, now)) {
185
+ this.recordMissedLate(record, now);
186
+ return;
187
+ }
161
188
  this.fire(record, now);
162
189
  }
163
190
  fire(record, now) {
@@ -243,10 +270,17 @@ export class SchedulerEngine {
243
270
  store.recordFire(record.id, { at: new Date(now).toISOString(), status: 'skipped-overlap', boardId: null, error: 'previous run for this schedule is still in flight' }, this.nextFireAtAfterStart(record, now));
244
271
  this.broadcastSchedules();
245
272
  }
273
+ recordMissedLate(record, now) {
274
+ const store = this.store;
275
+ if (!store)
276
+ return;
277
+ store.recordFire(record.id, { at: new Date(now).toISOString(), status: 'missed', boardId: null, error: SCHEDULE_MISSED_LATENESS_ERROR }, this.nextFireAtAfterStart(record, now));
278
+ this.broadcastSchedules();
279
+ }
246
280
  nextFireAtAfterStart(record, now) {
247
- if (record.timing.kind === 'interval')
248
- return new Date(now + record.timing.everyMs).toISOString();
249
- return null;
281
+ if (!isRecurringTiming(record.timing))
282
+ return null;
283
+ return computeNextFireAt(record.timing, new Date(now));
250
284
  }
251
285
  nextFireAtAfterFailure(record, now, consecutiveEmptyRuns) {
252
286
  if (record.timing.kind === 'once')
@@ -254,7 +288,7 @@ export class SchedulerEngine {
254
288
  if (record.timing.kind === 'continuous') {
255
289
  return new Date(now + continuousDelayMs(consecutiveEmptyRuns)).toISOString();
256
290
  }
257
- return new Date(now + record.timing.everyMs).toISOString();
291
+ return computeNextFireAt(record.timing, new Date(now));
258
292
  }
259
293
  nextFireAtAfterSettle(record, now, consecutiveEmptyRuns) {
260
294
  if (record.timing.kind === 'once')
@@ -263,7 +297,7 @@ export class SchedulerEngine {
263
297
  return new Date(now + continuousDelayMs(consecutiveEmptyRuns)).toISOString();
264
298
  }
265
299
  const store = this.store;
266
- return store?.get(record.id)?.nextFireAt ?? new Date(now + record.timing.everyMs).toISOString();
300
+ return store?.get(record.id)?.nextFireAt ?? computeNextFireAt(record.timing, new Date(now));
267
301
  }
268
302
  broadcastSchedules() {
269
303
  if (!this.store)
@@ -0,0 +1,294 @@
1
+ // Copyright (c) 2025-present Minionry, Inc.
2
+ // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
+ import { isValidMaxLatenessMs, } from '../schedule/agent-schedule-store.js';
4
+ import { normalizeWallClockTiming } from '../schedule/daily-timing.js';
5
+ import { isValidPromptFilePath, readPromptFile } from '../schedule/prompt-file.js';
6
+ import { logAppAudit } from './audit.js';
7
+ import { guardWorkspacePath, WORKSPACE_PATH_PREFIX } from './files-workspace.js';
8
+ import { SdkMethodError } from './registry.js';
9
+ function toAgentScheduleSnapshot(record) {
10
+ const snapshot = {
11
+ id: record.id,
12
+ createdAt: record.createdAt,
13
+ prompt: record.prompt,
14
+ timing: record.timing,
15
+ enabled: record.enabled,
16
+ nextFireAt: record.nextFireAt,
17
+ history: record.history.map((entry) => ({ ...entry })),
18
+ };
19
+ if (record.name !== undefined)
20
+ snapshot.name = record.name;
21
+ if (record.promptFile !== undefined)
22
+ snapshot.promptFile = `${WORKSPACE_PATH_PREFIX}/${record.promptFile}`;
23
+ if (record.lastFire !== undefined)
24
+ snapshot.lastFire = { ...record.lastFire };
25
+ if (record.maxLatenessMs !== undefined)
26
+ snapshot.maxLatenessMs = record.maxLatenessMs;
27
+ return snapshot;
28
+ }
29
+ function ownedByCaller(record, appId) {
30
+ return record.initiator?.appId === appId;
31
+ }
32
+ let scheduleHost = null;
33
+ export function setSdkAgentScheduleHost(host) {
34
+ scheduleHost = host;
35
+ }
36
+ export function resetSdkAgentScheduleHost() {
37
+ scheduleHost = null;
38
+ }
39
+ function requireScheduleHost(method) {
40
+ if (!scheduleHost) {
41
+ throw new SdkMethodError('INTERNAL', `'${method}' host is not wired — the WebSocket handler installs it at startup`);
42
+ }
43
+ return scheduleHost;
44
+ }
45
+ function paramsObject(params) {
46
+ return typeof params === 'object' && params !== null && !Array.isArray(params)
47
+ ? params
48
+ : {};
49
+ }
50
+ function parseScheduleIdParams(params, method) {
51
+ const raw = paramsObject(params);
52
+ if (typeof raw.scheduleId !== 'string' || raw.scheduleId.trim() === '') {
53
+ throw new SdkMethodError('BAD_REQUEST', `${method} requires params { scheduleId }`);
54
+ }
55
+ return raw.scheduleId;
56
+ }
57
+ function scheduleNotFound(scheduleId) {
58
+ return new SdkMethodError('BAD_REQUEST', `schedule '${scheduleId}' not found in this Space`);
59
+ }
60
+ function parseTiming(raw, method) {
61
+ if (raw && typeof raw === 'object') {
62
+ const t = raw;
63
+ if (t.kind === 'once' && typeof t.at === 'string' && !Number.isNaN(Date.parse(t.at))) {
64
+ return { kind: 'once', at: t.at };
65
+ }
66
+ if (t.kind === 'interval' && typeof t.everyMs === 'number' && Number.isFinite(t.everyMs)) {
67
+ return { kind: 'interval', everyMs: t.everyMs };
68
+ }
69
+ if (t.kind === 'daily' || t.kind === 'monthly') {
70
+ const normalized = normalizeWallClockTiming(raw);
71
+ if (normalized)
72
+ return normalized;
73
+ }
74
+ }
75
+ throw new SdkMethodError('BAD_REQUEST', `${method}: timing must be {kind:'once',at}, {kind:'interval',everyMs}, {kind:'daily',time,timeZone,days?} or {kind:'monthly',time,timeZone,dayOfMonth} — 'continuous' is not supported for agent-prompt schedules`);
76
+ }
77
+ function parseMaxLatenessCreateField(raw, method) {
78
+ if (raw === undefined)
79
+ return undefined;
80
+ if (!isValidMaxLatenessMs(raw)) {
81
+ throw new SdkMethodError('BAD_REQUEST', `${method}: maxLatenessMs must be an integer >= the schedule minimum`);
82
+ }
83
+ return raw;
84
+ }
85
+ function parseMaxLatenessPatchValue(raw, method) {
86
+ if (raw === null)
87
+ return null;
88
+ if (!isValidMaxLatenessMs(raw)) {
89
+ throw new SdkMethodError('BAD_REQUEST', `${method}: maxLatenessMs must be an integer >= the schedule minimum, or null to clear it`);
90
+ }
91
+ return raw;
92
+ }
93
+ function resolvePromptFileParam(workingDir, raw, method) {
94
+ if (typeof raw !== 'string' || !raw.startsWith(WORKSPACE_PATH_PREFIX)) {
95
+ throw new SdkMethodError('BAD_REQUEST', `${method}: promptFile must be a workspace-rooted path ('${WORKSPACE_PATH_PREFIX}/<path>')`);
96
+ }
97
+ const rel = raw.slice(WORKSPACE_PATH_PREFIX.length).replace(/^\/+/, '');
98
+ if (!isValidPromptFilePath(rel)) {
99
+ throw new SdkMethodError('BAD_REQUEST', `${method}: promptFile '${raw}' must not contain a '..' segment`);
100
+ }
101
+ guardWorkspacePath(workingDir, rel);
102
+ const result = readPromptFile(workingDir, rel);
103
+ if (!result.ok) {
104
+ throw new SdkMethodError('BAD_REQUEST', `${method}: ${result.reason}`);
105
+ }
106
+ return { prompt: result.content, promptFile: rel };
107
+ }
108
+ function parseCreateParams(args) {
109
+ const raw = paramsObject(args.params);
110
+ const hasPrompt = raw.prompt !== undefined;
111
+ const hasPromptFile = raw.promptFile !== undefined;
112
+ if (hasPrompt === hasPromptFile) {
113
+ throw new SdkMethodError('BAD_REQUEST', `${args.method} requires exactly one of params.prompt or params.promptFile`);
114
+ }
115
+ let prompt;
116
+ let promptFile;
117
+ if (hasPromptFile) {
118
+ const resolved = resolvePromptFileParam(args.workingDir, raw.promptFile, args.method);
119
+ prompt = resolved.prompt;
120
+ promptFile = resolved.promptFile;
121
+ }
122
+ else {
123
+ if (typeof raw.prompt !== 'string' || raw.prompt.trim() === '') {
124
+ throw new SdkMethodError('BAD_REQUEST', `${args.method}: params.prompt must be a non-empty string`);
125
+ }
126
+ prompt = raw.prompt.trim();
127
+ }
128
+ const timing = parseTiming(raw.timing, args.method);
129
+ const parsed = { prompt, timing };
130
+ if (promptFile !== undefined)
131
+ parsed.promptFile = promptFile;
132
+ if (typeof raw.name === 'string')
133
+ parsed.name = raw.name;
134
+ if (raw.enabled !== undefined) {
135
+ if (typeof raw.enabled !== 'boolean') {
136
+ throw new SdkMethodError('BAD_REQUEST', `${args.method}: enabled must be a boolean when present`);
137
+ }
138
+ parsed.enabled = raw.enabled;
139
+ }
140
+ const maxLatenessMs = parseMaxLatenessCreateField(raw.maxLatenessMs, args.method);
141
+ if (maxLatenessMs !== undefined)
142
+ parsed.maxLatenessMs = maxLatenessMs;
143
+ return parsed;
144
+ }
145
+ function applyContentPatchFields(args, patchObj, patch) {
146
+ if (patchObj.prompt !== undefined && patchObj.promptFile !== undefined) {
147
+ throw new SdkMethodError('BAD_REQUEST', `${args.method}: provide at most one of patch.prompt or patch.promptFile`);
148
+ }
149
+ if (patchObj.name !== undefined) {
150
+ if (typeof patchObj.name !== 'string') {
151
+ throw new SdkMethodError('BAD_REQUEST', `${args.method}: patch.name must be a string when present`);
152
+ }
153
+ patch.name = patchObj.name;
154
+ }
155
+ if (patchObj.prompt !== undefined) {
156
+ if (typeof patchObj.prompt !== 'string' || patchObj.prompt.trim() === '') {
157
+ throw new SdkMethodError('BAD_REQUEST', `${args.method}: patch.prompt must be a non-empty string`);
158
+ }
159
+ patch.prompt = patchObj.prompt.trim();
160
+ }
161
+ if (patchObj.promptFile !== undefined) {
162
+ const resolved = resolvePromptFileParam(args.workingDir, patchObj.promptFile, args.method);
163
+ patch.prompt = resolved.prompt;
164
+ patch.promptFile = resolved.promptFile;
165
+ }
166
+ }
167
+ function applyStructuredPatchFields(args, patchObj, patch) {
168
+ if (patchObj.enabled !== undefined) {
169
+ if (typeof patchObj.enabled !== 'boolean') {
170
+ throw new SdkMethodError('BAD_REQUEST', `${args.method}: patch.enabled must be a boolean when present`);
171
+ }
172
+ patch.enabled = patchObj.enabled;
173
+ }
174
+ if (patchObj.timing !== undefined)
175
+ patch.timing = parseTiming(patchObj.timing, args.method);
176
+ if (patchObj.maxLatenessMs !== undefined) {
177
+ patch.maxLatenessMs = parseMaxLatenessPatchValue(patchObj.maxLatenessMs, args.method);
178
+ }
179
+ }
180
+ function parseUpdateParams(args) {
181
+ const scheduleId = parseScheduleIdParams(args.params, args.method);
182
+ const raw = paramsObject(args.params);
183
+ const rawPatch = raw.patch;
184
+ if (typeof rawPatch !== 'object' || rawPatch === null || Array.isArray(rawPatch)) {
185
+ throw new SdkMethodError('BAD_REQUEST', `${args.method} requires params.patch to be an object`);
186
+ }
187
+ const patchObj = rawPatch;
188
+ const patch = {};
189
+ applyContentPatchFields(args, patchObj, patch);
190
+ applyStructuredPatchFields(args, patchObj, patch);
191
+ return { scheduleId, patch };
192
+ }
193
+ function paramsCarryPromptFile(params) {
194
+ const raw = paramsObject(params);
195
+ if (raw.promptFile !== undefined)
196
+ return true;
197
+ const patch = paramsObject(raw.patch);
198
+ return patch.promptFile !== undefined;
199
+ }
200
+ export function resolveAgentSchedulesScopes(params) {
201
+ return paramsCarryPromptFile(params) ? ['agents:schedule', 'files:workspace:read'] : ['agents:schedule'];
202
+ }
203
+ export function agentSchedulesListHandler(args) {
204
+ const host = requireScheduleHost(args.method);
205
+ const store = host.getStore(args.workingDir);
206
+ const schedules = store
207
+ .list()
208
+ .filter((record) => ownedByCaller(record, args.appId))
209
+ .map(toAgentScheduleSnapshot);
210
+ return { schedules };
211
+ }
212
+ export function agentSchedulesCreateHandler(args) {
213
+ const host = requireScheduleHost(args.method);
214
+ const { name, prompt, promptFile, timing, enabled, maxLatenessMs } = parseCreateParams(args);
215
+ const store = host.getStore(args.workingDir);
216
+ const record = store.create({
217
+ name,
218
+ prompt,
219
+ ...(promptFile !== undefined ? { promptFile } : {}),
220
+ initiator: { appId: args.appId, origin: args.origin },
221
+ timing,
222
+ enabled,
223
+ maxLatenessMs,
224
+ });
225
+ host.notifyAndBroadcast(args.workingDir);
226
+ logAppAudit({
227
+ appId: args.appId,
228
+ origin: args.origin,
229
+ method: 'agents.schedules.create.created',
230
+ decision: 'allow',
231
+ requestId: args.requestId,
232
+ spaceKey: args.spaceKey,
233
+ reason: `schedule ${record.id} created (nextFireAt=${record.nextFireAt ?? 'none'})`,
234
+ });
235
+ return toAgentScheduleSnapshot(record);
236
+ }
237
+ export function agentSchedulesUpdateHandler(args) {
238
+ const host = requireScheduleHost(args.method);
239
+ const { scheduleId, patch } = parseUpdateParams(args);
240
+ const store = host.getStore(args.workingDir);
241
+ const existing = store.get(scheduleId);
242
+ if (!existing || !ownedByCaller(existing, args.appId))
243
+ throw scheduleNotFound(scheduleId);
244
+ const record = store.update(scheduleId, patch);
245
+ if (!record)
246
+ throw scheduleNotFound(scheduleId);
247
+ host.notifyAndBroadcast(args.workingDir);
248
+ return toAgentScheduleSnapshot(record);
249
+ }
250
+ export function agentSchedulesDeleteHandler(args) {
251
+ const host = requireScheduleHost(args.method);
252
+ const scheduleId = parseScheduleIdParams(args.params, args.method);
253
+ const store = host.getStore(args.workingDir);
254
+ const existing = store.get(scheduleId);
255
+ if (!existing || !ownedByCaller(existing, args.appId))
256
+ throw scheduleNotFound(scheduleId);
257
+ if (!store.delete(scheduleId))
258
+ throw scheduleNotFound(scheduleId);
259
+ host.notifyAndBroadcast(args.workingDir);
260
+ logAppAudit({
261
+ appId: args.appId,
262
+ origin: args.origin,
263
+ method: 'agents.schedules.delete.deleted',
264
+ decision: 'allow',
265
+ requestId: args.requestId,
266
+ spaceKey: args.spaceKey,
267
+ reason: `schedule ${scheduleId} deleted`,
268
+ });
269
+ return null;
270
+ }
271
+ export function agentSchedulesRunNowHandler(args) {
272
+ const host = requireScheduleHost(args.method);
273
+ const scheduleId = parseScheduleIdParams(args.params, args.method);
274
+ const store = host.getStore(args.workingDir);
275
+ const existing = store.get(scheduleId);
276
+ if (!existing || !ownedByCaller(existing, args.appId))
277
+ throw scheduleNotFound(scheduleId);
278
+ const result = host.fireNow(args.workingDir, scheduleId);
279
+ if (result.kind === 'not-found')
280
+ throw scheduleNotFound(scheduleId);
281
+ if (result.kind === 'disabled') {
282
+ throw new SdkMethodError('BAD_REQUEST', `schedule '${scheduleId}' is disabled — enable it before running it now`);
283
+ }
284
+ logAppAudit({
285
+ appId: args.appId,
286
+ origin: args.origin,
287
+ method: 'agents.schedules.runNow.fired',
288
+ decision: 'allow',
289
+ requestId: args.requestId,
290
+ spaceKey: args.spaceKey,
291
+ reason: `schedule ${scheduleId} fired on demand`,
292
+ });
293
+ return toAgentScheduleSnapshot(result.record);
294
+ }
@@ -7,6 +7,7 @@ import { hermesAvailability, MINIMUM_HERMES_APP_RUN_VERSION } from '../../engine
7
7
  import { getQualityEffortLevel, getQualityEngine, getQualityFastMode, getQualityModel } from '../settings.js';
8
8
  import { resolveAppOwnedModel } from './app-endpoints.js';
9
9
  import { logAppAudit } from './audit.js';
10
+ import { resolveAutomationTarget } from './browser.js';
10
11
  import { deregisterLiveRun, findLiveRunByTab, listLiveSubscriptions, registerLiveRun } from './live-registry.js';
11
12
  import { SdkMethodError } from './registry.js';
12
13
  import { dropSubscription } from './revoke.js';
@@ -90,17 +91,19 @@ function cancelRunRecord(record, cause) {
90
91
  record.cancelledBy = cause;
91
92
  record.cancelRun();
92
93
  }
93
- const RUN_PARAMS_SHAPE = '{ prompt, title?, model?, modelSelection?, systemPrompt?, budget?: { maxTurns?, maxSeconds? }, sessionId? }';
94
+ const RUN_PARAMS_SHAPE = '{ prompt, title?, model?, modelSelection?, engineSelection?: { engine?, model?, effortLevel?, fastMode? }, systemPrompt?, budget?: { maxTurns?, maxSeconds? }, sessionId? }';
94
95
  const RUN_PARAM_KEYS = new Set([
95
96
  'prompt',
96
97
  'title',
97
98
  'model',
98
99
  'modelSelection',
100
+ 'engineSelection',
99
101
  'systemPrompt',
100
102
  'budget',
101
103
  'sessionId',
102
104
  ]);
103
105
  const BUDGET_KEYS = new Set(['maxTurns', 'maxSeconds']);
106
+ const ENGINE_SELECTION_KEYS = new Set(['engine', 'model', 'effortLevel', 'fastMode']);
104
107
  function badRequest(message) {
105
108
  return new SdkMethodError('BAD_REQUEST', message);
106
109
  }
@@ -161,14 +164,50 @@ function parseModelSelection(raw) {
161
164
  }
162
165
  return name;
163
166
  }
164
- function assertRunFieldsCombine({ model, modelSelection, systemPrompt, budget, sessionId }) {
165
- const choosesModel = model !== undefined || modelSelection !== undefined;
167
+ function optionalEngineSelectionString(raw, field) {
168
+ const value = raw[field];
169
+ if (value === undefined)
170
+ return undefined;
171
+ if (typeof value !== 'string' || value === '') {
172
+ throw badRequest(`agents.run engineSelection.${field} must be a non-empty string when present`);
173
+ }
174
+ return value;
175
+ }
176
+ function parseEngineSelection(raw) {
177
+ if (raw === undefined)
178
+ return undefined;
179
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
180
+ throw badRequest('agents.run engineSelection must be an object { engine?, model?, effortLevel?, fastMode? }');
181
+ }
182
+ const obj = raw;
183
+ refuseUnknownKeys(obj, ENGINE_SELECTION_KEYS, 'agents.run engineSelection', '{ engine?, model?, effortLevel?, fastMode? }');
184
+ const engine = optionalEngineSelectionString(obj, 'engine');
185
+ const model = optionalEngineSelectionString(obj, 'model');
186
+ const effortLevel = optionalEngineSelectionString(obj, 'effortLevel');
187
+ if (obj.fastMode !== undefined && typeof obj.fastMode !== 'boolean') {
188
+ throw badRequest('agents.run engineSelection.fastMode must be a boolean when present');
189
+ }
190
+ return {
191
+ ...(engine !== undefined ? { engine } : {}),
192
+ ...(model !== undefined ? { model } : {}),
193
+ ...(effortLevel !== undefined ? { effortLevel } : {}),
194
+ ...(obj.fastMode !== undefined ? { fastMode: obj.fastMode } : {}),
195
+ };
196
+ }
197
+ function assertRunFieldsCombine({ model, modelSelection, engineSelection, systemPrompt, budget, sessionId }) {
198
+ const choosesModel = model !== undefined || modelSelection !== undefined || engineSelection !== undefined;
166
199
  if (sessionId !== undefined && (choosesModel || systemPrompt !== undefined || budget !== undefined)) {
167
- throw badRequest('agents.run with sessionId continues that session on its own model, systemPrompt and budget — omit model, modelSelection, systemPrompt and budget');
200
+ throw badRequest('agents.run with sessionId continues that session on its own model, systemPrompt and budget — omit model, modelSelection, engineSelection, systemPrompt and budget');
168
201
  }
169
202
  if (model !== undefined && modelSelection !== undefined) {
170
203
  throw badRequest('agents.run takes model or modelSelection, not both');
171
204
  }
205
+ if (model !== undefined && engineSelection !== undefined) {
206
+ throw badRequest('agents.run takes model or engineSelection, not both');
207
+ }
208
+ if (modelSelection !== undefined && engineSelection !== undefined) {
209
+ throw badRequest('agents.run takes modelSelection or engineSelection, not both');
210
+ }
172
211
  if (model === undefined && (systemPrompt !== undefined || budget !== undefined)) {
173
212
  throw badRequest('agents.run systemPrompt and budget apply only to a run on an app-registered model — pass model');
174
213
  }
@@ -185,15 +224,17 @@ function parseRunParams(params) {
185
224
  const title = optionalString(raw, 'title', false);
186
225
  const model = optionalString(raw, 'model', true);
187
226
  const modelSelection = parseModelSelection(raw.modelSelection);
227
+ const engineSelection = parseEngineSelection(raw.engineSelection);
188
228
  const sessionId = optionalString(raw, 'sessionId', true);
189
229
  const systemPrompt = parseSystemPrompt(raw.systemPrompt);
190
230
  const budget = raw.budget === undefined ? undefined : parseBudget(raw.budget);
191
- assertRunFieldsCombine({ model, modelSelection, systemPrompt, budget, sessionId });
231
+ assertRunFieldsCombine({ model, modelSelection, engineSelection, systemPrompt, budget, sessionId });
192
232
  return {
193
233
  prompt: raw.prompt,
194
234
  ...(title !== undefined ? { title } : {}),
195
235
  ...(model !== undefined ? { model } : {}),
196
236
  ...(modelSelection !== undefined ? { modelSelection } : {}),
237
+ ...(engineSelection !== undefined ? { engineSelection } : {}),
197
238
  ...(systemPrompt !== undefined ? { systemPrompt } : {}),
198
239
  ...(budget !== undefined ? { budget } : {}),
199
240
  ...(sessionId !== undefined ? { sessionId } : {}),
@@ -226,15 +267,26 @@ async function admitAppOwnedModel(modelValue, args) {
226
267
  }
227
268
  export function resolveAgentsRunScopes(params) {
228
269
  const raw = typeof params === 'object' && params !== null && !Array.isArray(params) ? params : {};
229
- return raw.modelSelection === undefined ? ['agents:run'] : ['agents:run', 'quality:read'];
270
+ const scopes = ['agents:run'];
271
+ if (raw.modelSelection !== undefined)
272
+ scopes.push('quality:read');
273
+ if (raw.engineSelection !== undefined)
274
+ scopes.push('settings:read');
275
+ return scopes;
276
+ }
277
+ export function appRunEngines() {
278
+ return ['claude-code'];
230
279
  }
231
- function assertUserClaudeProfile(model) {
280
+ function engineUnavailableForAppRun(engine) {
281
+ return new SdkMethodError('ENGINE_UNAVAILABLE', `agents.run: ${engine} cannot run an app session with tools on this host — choose one of ${appRunEngines().join(', ') || '(none)'}`);
282
+ }
283
+ function assertUserOwnedProfile(model, engine) {
232
284
  const parsed = parseProfileModelValue(model);
233
285
  if (!parsed)
234
286
  return;
235
287
  const profile = new ModelProfileStore().get(parsed.profileId);
236
- if (!profile || profile.owner || profile.engine !== 'claude-code' || parsed.engine !== 'claude-code') {
237
- throw new SdkMethodError('MODEL_NOT_ALLOWED', `agents.run: the Quality model '${displayValue(model)}' is not a Claude Code model added on this machine — choose another model for Quality`);
288
+ if (!profile || profile.owner || profile.engine !== engine || parsed.engine !== engine) {
289
+ throw new SdkMethodError('MODEL_NOT_ALLOWED', `agents.run: the model '${displayValue(model)}' is not a ${engine} model added on this machine — choose another model`);
238
290
  }
239
291
  }
240
292
  function admitModelSelection(name, args) {
@@ -243,15 +295,16 @@ function admitModelSelection(name, args) {
243
295
  throw new SdkMethodError('SCOPE_DENIED', `agents.run: modelSelection '${name}' needs the quality:read scope`);
244
296
  }
245
297
  const engine = getQualityEngine();
246
- if (engine !== 'claude-code') {
247
- throw new SdkMethodError('ENGINE_UNAVAILABLE', `agents.run: the Quality model runs on ${engine}, and only Claude Code can run an app session with tools — choose a Claude Code model for Quality`);
298
+ if (!appRunEngines().includes(engine)) {
299
+ throw engineUnavailableForAppRun(engine);
248
300
  }
249
301
  const stored = getQualityModel();
250
302
  const model = stored !== 'default' && engineForModel(stored) === engine ? stored : undefined;
251
303
  if (model)
252
- assertUserClaudeProfile(model);
304
+ assertUserOwnedProfile(model, engine);
253
305
  const effortLevel = getQualityEffortLevel();
254
306
  return {
307
+ origin: 'quality',
255
308
  name,
256
309
  engine,
257
310
  ...(model ? { model } : {}),
@@ -259,6 +312,32 @@ function admitModelSelection(name, args) {
259
312
  fastMode: getQualityFastMode(),
260
313
  };
261
314
  }
315
+ function admitEngineSelection(raw, args) {
316
+ const granted = args.grant?.scopes ?? [];
317
+ if (!granted.includes('settings:read')) {
318
+ throw new SdkMethodError('SCOPE_DENIED', 'agents.run: engineSelection needs the settings:read scope');
319
+ }
320
+ const { engine, model } = resolveAutomationTarget(raw.engine, raw.model, 'agents.run engineSelection');
321
+ if (!appRunEngines().includes(engine)) {
322
+ throw engineUnavailableForAppRun(engine);
323
+ }
324
+ if (model)
325
+ assertUserOwnedProfile(model, engine);
326
+ if (raw.effortLevel !== undefined && raw.effortLevel !== 'auto' && !ENGINE_EFFORT_LEVELS[engine].includes(raw.effortLevel)) {
327
+ const accepted = ENGINE_EFFORT_LEVELS[engine].length > 0 ? ENGINE_EFFORT_LEVELS[engine].join(', ') : 'auto';
328
+ throw badRequest(`agents.run: engineSelection.effortLevel '${displayValue(raw.effortLevel)}' is not a ${engine} effort level — expected one of ${accepted}`);
329
+ }
330
+ if (raw.fastMode === true && engine !== 'claude-code') {
331
+ throw badRequest(`agents.run: engineSelection.fastMode is a claude-code setting — ${engine} has no fast mode`);
332
+ }
333
+ return {
334
+ origin: 'app',
335
+ engine,
336
+ ...(model ? { model } : {}),
337
+ effortLevel: raw.effortLevel ?? 'auto',
338
+ fastMode: raw.fastMode === true,
339
+ };
340
+ }
262
341
  function findAppRunSession(args, sessionId) {
263
342
  const session = sessionsByTab.get(sessionId);
264
343
  if (!session || session.appId !== args.appId || session.spaceKey !== args.spaceKey) {
@@ -299,6 +378,9 @@ export async function admitAgentRun(args, claim = false) {
299
378
  if (request.modelSelection !== undefined) {
300
379
  return { kind: 'start', request, selection: admitModelSelection(request.modelSelection, args) };
301
380
  }
381
+ if (request.engineSelection !== undefined) {
382
+ return { kind: 'start', request, selection: admitEngineSelection(request.engineSelection, args) };
383
+ }
302
384
  if (request.model === undefined)
303
385
  return { kind: 'start', request };
304
386
  const engine = await admitAppOwnedModel(request.model, args);
@@ -448,7 +530,7 @@ function runStartReason(record, ceiling, continued, model) {
448
530
  function selectionAuditSuffix(selection) {
449
531
  if (!selection)
450
532
  return '';
451
- return ` selection=${selection.name} engine=${selection.engine} model=${displayValue(selection.model ?? 'default')}`;
533
+ return ` selection=${selection.origin} engine=${selection.engine} model=${displayValue(selection.model ?? 'default')}`;
452
534
  }
453
535
  export function agentsResultHandler(args) {
454
536
  const raw = (typeof args.params === 'object' && args.params !== null && !Array.isArray(args.params))