@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
@@ -2,11 +2,14 @@
2
2
  // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
3
  import { readdirSync, realpathSync, statSync } from 'node:fs';
4
4
  import { basename, resolve, sep } from 'node:path';
5
- import { createDirectory, deleteFile, listDirectory, renameFile, writeFile } from '../files.js';
5
+ import { validatePathWithinRoots } from '../file-explorer-ops.js';
6
+ import { createDirectory, deleteFile, listDirectory, renameFile } from '../files.js';
7
+ import { containsDangerousPatterns } from '../pathUtils.js';
6
8
  import { SPACE_DIRNAME, SPACE_LAYOUT } from '../space-layout.js';
7
9
  import { readFileContent } from '../websocket/file-utils.js';
10
+ import { parseExpectedModifiedAt, writeFileAtomic } from './atomic-write.js';
8
11
  import { notifyFileChangedAbsolute } from './file-change-notify.js';
9
- import { FILES_APP_MAX_WRITE_BYTES, filesDeleteHandler, filesListHandler, filesMkdirHandler, filesReadEntryHandler, filesReadHandler, filesRenameHandler, filesWriteHandler, } from './files-app.js';
12
+ import { FILES_APP_MAX_WRITE_BYTES, filesDeleteHandler, filesListHandler, filesMkdirHandler, filesReadEntryHandler, filesReadHandler, filesRenameHandler, filesWriteHandler, readEntryResult, } from './files-app.js';
10
13
  import { SdkMethodError } from './registry.js';
11
14
  import { resolveWithinRoot } from './sandbox.js';
12
15
  export const WORKSPACE_PATH_PREFIX = 'workspace:';
@@ -45,7 +48,7 @@ function inSpaceAppArea(segments) {
45
48
  }
46
49
  return false;
47
50
  }
48
- export function guardWorkspacePath(workingDir, rel) {
51
+ export function guardWorkspacePath(workingDir, rel, opts) {
49
52
  let realWorkingDir;
50
53
  try {
51
54
  realWorkingDir = realpathSync(resolve(workingDir));
@@ -53,7 +56,7 @@ export function guardWorkspacePath(workingDir, rel) {
53
56
  catch (error) {
54
57
  throw new SdkMethodError('INTERNAL', `Space working directory unavailable: ${error instanceof Error ? error.message : String(error)}`);
55
58
  }
56
- const guarded = resolveWithinRoot(realWorkingDir, rel);
59
+ const guarded = resolveWithinRoot(realWorkingDir, rel, opts);
57
60
  if (!guarded.valid) {
58
61
  throw new SdkMethodError('SCOPE_DENIED', 'Access denied: path escapes the Space workspace');
59
62
  }
@@ -127,6 +130,17 @@ export function filesReadRoutedHandler(args) {
127
130
  throw serviceError(result.error);
128
131
  return result.content;
129
132
  }
133
+ function refuseLikeWriteFile(resolvedPath, realWorkingDir) {
134
+ if (containsDangerousPatterns(resolvedPath)) {
135
+ console.error(`[FileService] SECURITY: Dangerous pattern in path: "${resolvedPath}"`);
136
+ throw serviceError('Invalid path: contains dangerous patterns');
137
+ }
138
+ const validation = validatePathWithinRoots(resolvedPath, realWorkingDir, []);
139
+ if (!validation.valid)
140
+ throw serviceError(validation.error);
141
+ if (statOrUndefined(resolvedPath)?.isDirectory())
142
+ throw serviceError('Cannot write to a directory');
143
+ }
130
144
  export function filesWriteRoutedHandler(args) {
131
145
  const target = parseWorkspacePath(args.params);
132
146
  if (target === null)
@@ -135,14 +149,21 @@ export function filesWriteRoutedHandler(args) {
135
149
  if (typeof raw.content !== 'string') {
136
150
  throw new SdkMethodError('BAD_REQUEST', `${args.method} requires params { path, content } with string content`);
137
151
  }
152
+ const expectedModifiedAt = parseExpectedModifiedAt(raw, args.method);
138
153
  const bytes = Buffer.byteLength(raw.content, 'utf-8');
139
154
  if (bytes > FILES_APP_MAX_WRITE_BYTES) {
140
155
  throw new SdkMethodError('BAD_REQUEST', `content is ${bytes} bytes — exceeds the ${FILES_APP_MAX_WRITE_BYTES}-byte per-file files.write cap`);
141
156
  }
142
157
  const { realWorkingDir, resolvedPath } = guardWorkspacePath(args.workingDir, target.rel);
143
- const result = writeFile(resolvedPath, raw.content, realWorkingDir);
144
- if (!result.success)
145
- throw serviceError(result.error);
158
+ refuseLikeWriteFile(resolvedPath, realWorkingDir);
159
+ try {
160
+ writeFileAtomic({ target: resolvedPath, content: raw.content, expectedModifiedAt, displayPath: target.original });
161
+ }
162
+ catch (error) {
163
+ if (error instanceof SdkMethodError)
164
+ throw error;
165
+ throw serviceError(error instanceof Error ? error.message : undefined);
166
+ }
146
167
  notifyFileChangedAbsolute(args.spaceKey, resolvedPath, 'changed');
147
168
  return null;
148
169
  }
@@ -182,9 +203,7 @@ export function filesReadEntryRoutedHandler(args) {
182
203
  const result = readFileContent(resolvedPath, realWorkingDir);
183
204
  if (result.error)
184
205
  throw serviceError(result.error);
185
- return result.isImage
186
- ? { kind: 'binary', content: result.content, mimeType: result.mimeType ?? 'application/octet-stream' }
187
- : { kind: 'text', content: result.content };
206
+ return readEntryResult(result, stat);
188
207
  }
189
208
  export function filesMkdirRoutedHandler(args) {
190
209
  const target = parseWorkspacePath(args.params);
@@ -194,6 +213,7 @@ export function filesMkdirRoutedHandler(args) {
194
213
  const result = createDirectory(resolvedPath, realWorkingDir);
195
214
  if (!result.success)
196
215
  throw serviceError(result.error);
216
+ notifyFileChangedAbsolute(args.spaceKey, resolvedPath, 'created', { dir: true });
197
217
  return null;
198
218
  }
199
219
  export function filesRenameRoutedHandler(args) {
@@ -216,6 +236,10 @@ export function filesRenameRoutedHandler(args) {
216
236
  const result = renameFile(resolvedOld, resolvedNew, realWorkingDir);
217
237
  if (!result.success)
218
238
  throw serviceError(result.error);
239
+ notifyFileChangedAbsolute(args.spaceKey, resolvedOld, 'renamed', {
240
+ newPath: resolvedNew,
241
+ dir: statOrUndefined(resolvedNew)?.isDirectory(),
242
+ });
219
243
  return null;
220
244
  }
221
245
  export function filesDeleteRoutedHandler(args) {
@@ -223,11 +247,13 @@ export function filesDeleteRoutedHandler(args) {
223
247
  if (target === null)
224
248
  return filesDeleteHandler(args);
225
249
  const { realWorkingDir, resolvedPath } = guardWorkspacePath(args.workingDir, target.rel);
226
- if (!statOrUndefined(resolvedPath))
250
+ const stat = statOrUndefined(resolvedPath);
251
+ if (!stat)
227
252
  throw notFound(target.original);
228
253
  refuseAppAreaTree(resolvedPath);
229
254
  const result = deleteFile(resolvedPath, realWorkingDir);
230
255
  if (!result.success)
231
256
  throw serviceError(result.error);
257
+ notifyFileChangedAbsolute(args.spaceKey, resolvedPath, 'deleted', { dir: stat.isDirectory() });
232
258
  return null;
233
259
  }
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { buildSpawnEnv, resolveClaudeEffort } from '../../cli/headless/claude-invoker-process.js';
8
8
  import { classifyEngineNotice, isSyntheticModel } from '../../cli/headless/engine-notice.js';
9
- import { resolveClaudeCommand } from '../../engines/claude/claude-command.js';
9
+ import { resolveClaudeCommand, TOOL_LESS_CLAUDE_ARGS } from '../../engines/claude/claude-command.js';
10
10
  import { resolveModelProfile } from '../../engines/credentials/model-profiles.js';
11
11
  import { SdkInferenceError, } from './inference.js';
12
12
  const DEFAULT_SYSTEM_PROMPT = 'You are a helpful assistant.';
@@ -36,7 +36,9 @@ export function buildClaudeInferenceArgs(exec) {
36
36
  const effort = resolveClaudeEffort(exec.model, exec.effortLevel ?? 'auto', true);
37
37
  if (effort)
38
38
  args.push('--effort', effort);
39
- args.push('--tools', '', '--strict-mcp-config', '--no-session-persistence', '--system-prompt', exec.system?.trim() ? exec.system : DEFAULT_SYSTEM_PROMPT);
39
+ if (exec.fastMode)
40
+ args.push('--settings', JSON.stringify({ fastMode: true }));
41
+ args.push(...TOOL_LESS_CLAUDE_ARGS, '--no-session-persistence', '--system-prompt', exec.system?.trim() ? exec.system : DEFAULT_SYSTEM_PROMPT);
40
42
  return args;
41
43
  }
42
44
  function parseStreamLine(line) {
@@ -79,7 +79,7 @@ function settleRun(record, result) {
79
79
  }
80
80
  function paramsObject(params) {
81
81
  if (typeof params !== 'object' || params === null || Array.isArray(params)) {
82
- throw new SdkMethodError('BAD_REQUEST', 'inference.chat requires params { messages, system?, engine?, model?, effortLevel? }');
82
+ throw new SdkMethodError('BAD_REQUEST', 'inference.chat requires params { messages, system?, engine?, model?, effortLevel?, fastMode? }');
83
83
  }
84
84
  return params;
85
85
  }
@@ -122,6 +122,17 @@ function parseEffortLevel(raw, engine) {
122
122
  }
123
123
  return raw;
124
124
  }
125
+ function parseFastMode(raw, engine) {
126
+ if (raw === undefined)
127
+ return false;
128
+ if (typeof raw !== 'boolean') {
129
+ throw new SdkMethodError('BAD_REQUEST', 'inference.chat fastMode must be a boolean');
130
+ }
131
+ if (raw && engine !== 'claude-code') {
132
+ throw new SdkMethodError('BAD_REQUEST', `inference.chat: fastMode is a claude-code setting — ${engine} has no fast mode`);
133
+ }
134
+ return raw;
135
+ }
125
136
  export function inferenceBouncerInput(params) {
126
137
  const raw = typeof params === 'object' && params !== null && !Array.isArray(params) ? params : {};
127
138
  const messages = Array.isArray(raw.messages) ? raw.messages : [];
@@ -133,6 +144,7 @@ export function inferenceBouncerInput(params) {
133
144
  engine: typeof raw.engine === 'string' ? raw.engine : 'default',
134
145
  model: typeof raw.model === 'string' ? raw.model : 'default',
135
146
  effortLevel: typeof raw.effortLevel === 'string' ? raw.effortLevel : 'auto',
147
+ ...(raw.fastMode === true ? { fastMode: true } : {}),
136
148
  messages: messages.length,
137
149
  chars: contentChars + (typeof raw.system === 'string' ? raw.system.length : 0),
138
150
  };
@@ -145,6 +157,7 @@ export function inferenceChatHandler(args) {
145
157
  const messages = parseMessages(raw.messages);
146
158
  const system = parseSystem(raw.system);
147
159
  const { engine, model } = resolveAutomationTarget(raw.engine, raw.model, 'inference.chat');
160
+ const fastMode = parseFastMode(raw.fastMode, engine);
148
161
  const executor = executors[engine];
149
162
  if (!executor) {
150
163
  throw new SdkMethodError('UNSUPPORTED', `inference.chat: ${engine} cannot run a tool-less completion on this host — choose one of ${inferenceEngines().join(', ') || '(none)'}`);
@@ -186,7 +199,7 @@ export function inferenceChatHandler(args) {
186
199
  decision: 'allow',
187
200
  requestId: args.requestId,
188
201
  spaceKey: args.spaceKey,
189
- reason: `run ${runId} engine=${engine} model=${model ?? 'default'} effort=${effortLevel ?? 'auto'} messages=${messages.length} [tool-less]`,
202
+ reason: `run ${runId} engine=${engine} model=${model ?? 'default'} effort=${effortLevel ?? 'auto'}${fastMode ? ' fastMode' : ''} messages=${messages.length} [tool-less]`,
190
203
  });
191
204
  setImmediate(() => {
192
205
  if (record.outcome)
@@ -207,6 +220,7 @@ export function inferenceChatHandler(args) {
207
220
  engine,
208
221
  ...(model ? { model } : {}),
209
222
  ...(effortLevel ? { effortLevel } : {}),
223
+ ...(fastMode ? { fastMode } : {}),
210
224
  ...(system !== undefined ? { system } : {}),
211
225
  messages,
212
226
  signal: record.controller.signal,
@@ -1,8 +1,11 @@
1
1
  // Copyright (c) 2025-present Minionry, Inc.
2
2
  // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
+ import { basename } from 'node:path';
3
4
  import { resolvePmDir } from '../plan/parser.js';
4
- import { cleanupScheduleSource, isValidTiming, parseUpdatePatch, resolveCreateSource } from '../schedule/schedule-wire.js';
5
+ import { cleanupScheduleSource, isValidTiming, parseMaxLatenessMs, parseUpdatePatch, resolveCreateSource } from '../schedule/schedule-wire.js';
6
+ import { scaffoldPmDirectory } from '../websocket/plan-helpers.js';
5
7
  import { logAppAudit } from './audit.js';
8
+ import { guardWorkspacePath, WORKSPACE_PATH_PREFIX } from './files-workspace.js';
6
9
  import { parseBoardRunInput } from './pm.js';
7
10
  import { SdkMethodError } from './registry.js';
8
11
  function toScheduleSnapshot(record) {
@@ -21,6 +24,8 @@ function toScheduleSnapshot(record) {
21
24
  snapshot.input = record.input;
22
25
  if (record.lastFire !== undefined)
23
26
  snapshot.lastFire = { ...record.lastFire };
27
+ if (record.maxLatenessMs !== undefined)
28
+ snapshot.maxLatenessMs = record.maxLatenessMs;
24
29
  return snapshot;
25
30
  }
26
31
  let scheduleHost = null;
@@ -51,19 +56,41 @@ function parseScheduleIdParams(params, method) {
51
56
  function scheduleNotFound(scheduleId) {
52
57
  return new SdkMethodError('BAD_REQUEST', `schedule '${scheduleId}' not found in this Space`);
53
58
  }
54
- function parseCreateParams(params) {
55
- const raw = paramsObject(params);
56
- const rawSource = paramsObject(raw.source);
57
- if (rawSource.kind !== 'template-json' || typeof rawSource.content !== 'string' || rawSource.content === '') {
58
- throw new SdkMethodError('BAD_REQUEST', "pm.schedules.create requires params { source: { kind: 'template-json', content }, timing } — board-zip and path-addressed sources are not supported over the SDK");
59
+ function resolveBoardZipPathParam(workingDir, raw, method) {
60
+ if (typeof raw !== 'string' || !raw.startsWith(WORKSPACE_PATH_PREFIX)) {
61
+ throw new SdkMethodError('BAD_REQUEST', `${method}: a board-zip source.path must be a workspace-rooted path ('${WORKSPACE_PATH_PREFIX}/<path>')`);
62
+ }
63
+ const rel = raw.slice(WORKSPACE_PATH_PREFIX.length).replace(/^\/+/, '');
64
+ if (rel === '' || rel.split('/').some((segment) => segment === '..')) {
65
+ throw new SdkMethodError('BAD_REQUEST', `${method}: source.path '${raw}' must not contain a '..' segment`);
66
+ }
67
+ return guardWorkspacePath(workingDir, rel).resolvedPath;
68
+ }
69
+ function parseCreateSourceParam(rawSource, workingDir, method) {
70
+ const source = paramsObject(rawSource);
71
+ if (source.kind === 'template-json') {
72
+ if (typeof source.content !== 'string' || source.content === '') {
73
+ throw new SdkMethodError('BAD_REQUEST', `${method} requires params.source.content for a 'template-json' source`);
74
+ }
75
+ return { kind: 'template-json', content: source.content };
59
76
  }
77
+ if (source.kind === 'board-zip') {
78
+ return { kind: 'board-zip', path: resolveBoardZipPathParam(workingDir, source.path, method) };
79
+ }
80
+ throw new SdkMethodError('BAD_REQUEST', `${method} requires params.source of kind 'template-json' (with content) or 'board-zip' (with a workspace:-rooted path)`);
81
+ }
82
+ export function resolvePmSchedulesCreateScopes(params) {
83
+ const raw = paramsObject(params);
84
+ const source = paramsObject(raw.source);
85
+ return source.kind === 'board-zip' ? ['pm:schedule', 'files:workspace:read'] : ['pm:schedule'];
86
+ }
87
+ function parseCreateParams(args) {
88
+ const raw = paramsObject(args.params);
89
+ const source = parseCreateSourceParam(raw.source, args.workingDir, args.method);
60
90
  if (!isValidTiming(raw.timing)) {
61
91
  throw new SdkMethodError('BAD_REQUEST', 'pm.schedules.create requires a valid params.timing');
62
92
  }
63
- const parsed = {
64
- source: { kind: 'template-json', content: rawSource.content },
65
- timing: raw.timing,
66
- };
93
+ const parsed = { source, timing: raw.timing };
67
94
  if (typeof raw.name === 'string')
68
95
  parsed.name = raw.name;
69
96
  if (raw.enabled !== undefined) {
@@ -73,6 +100,12 @@ function parseCreateParams(params) {
73
100
  }
74
101
  if (raw.input !== undefined)
75
102
  parsed.input = parseBoardRunInput(raw.input, 'pm.schedules.create');
103
+ const maxLateness = parseMaxLatenessMs(raw.maxLatenessMs);
104
+ if (!maxLateness.ok) {
105
+ throw new SdkMethodError('BAD_REQUEST', 'pm.schedules.create: maxLatenessMs must be an integer >= the schedule minimum');
106
+ }
107
+ if (maxLateness.value !== undefined)
108
+ parsed.maxLatenessMs = maxLateness.value;
76
109
  return parsed;
77
110
  }
78
111
  function parseUpdateParams(params) {
@@ -99,10 +132,14 @@ export function pmSchedulesListHandler(args) {
99
132
  }
100
133
  export function pmSchedulesCreateHandler(args) {
101
134
  const host = requireScheduleHost(args.method);
102
- const { name, source, timing, input, enabled } = parseCreateParams(args.params);
103
- const pmDir = resolvePmDir(args.workingDir);
135
+ const { name, source, timing, input, enabled, maxLatenessMs } = parseCreateParams(args);
136
+ let pmDir = resolvePmDir(args.workingDir);
137
+ if (!pmDir) {
138
+ scaffoldPmDirectory(args.workingDir, basename(args.workingDir) || 'My Project');
139
+ pmDir = resolvePmDir(args.workingDir);
140
+ }
104
141
  if (!pmDir) {
105
- throw new SdkMethodError('BAD_REQUEST', 'pm.schedules.create: no PM directory in this Space yet — create a board first');
142
+ throw new SdkMethodError('INTERNAL', 'pm.schedules.create: failed to initialize the Space PM directory');
106
143
  }
107
144
  let resolved;
108
145
  try {
@@ -113,9 +150,9 @@ export function pmSchedulesCreateHandler(args) {
113
150
  }
114
151
  const store = host.getStore(args.workingDir);
115
152
  if (!store) {
116
- throw new SdkMethodError('BAD_REQUEST', 'pm.schedules.create: no PM directory in this Space yet — create a board first');
153
+ throw new SdkMethodError('INTERNAL', 'pm.schedules.create: PM directory initialization did not take effect');
117
154
  }
118
- const record = store.create({ name, source: resolved.source, input, timing, enabled });
155
+ const record = store.create({ name, source: resolved.source, input, timing, enabled, maxLatenessMs });
119
156
  host.notifyAndBroadcast(args.workingDir);
120
157
  logAppAudit({
121
158
  appId: args.appId,
@@ -163,3 +200,23 @@ export function pmSchedulesDeleteHandler(args) {
163
200
  });
164
201
  return null;
165
202
  }
203
+ export function pmSchedulesRunNowHandler(args) {
204
+ const host = requireScheduleHost(args.method);
205
+ const scheduleId = parseScheduleIdParams(args.params, args.method);
206
+ const result = host.fireNow(args.workingDir, scheduleId);
207
+ if (result.kind === 'not-found')
208
+ throw scheduleNotFound(scheduleId);
209
+ if (result.kind === 'disabled') {
210
+ throw new SdkMethodError('BAD_REQUEST', `schedule '${scheduleId}' is disabled — enable it before running it now`);
211
+ }
212
+ logAppAudit({
213
+ appId: args.appId,
214
+ origin: args.origin,
215
+ method: 'pm.schedules.runNow.fired',
216
+ decision: 'allow',
217
+ requestId: args.requestId,
218
+ spaceKey: args.spaceKey,
219
+ reason: `schedule ${scheduleId} fired on demand`,
220
+ });
221
+ return toScheduleSnapshot(result.record);
222
+ }
@@ -265,6 +265,27 @@ function issueNotFound(boardId, issueId) {
265
265
  function badRequest(message) {
266
266
  return new SdkMethodError('BAD_REQUEST', message);
267
267
  }
268
+ function assertReviewGateNotBypassed(args, issue, fields) {
269
+ if (issue.reviewGate !== 'required' || fields.status === undefined)
270
+ return;
271
+ const completesGate = fields.status === 'done';
272
+ const parked = issue.status === 'awaiting_approval';
273
+ if (!completesGate && !parked)
274
+ return;
275
+ const reason = completesGate
276
+ ? `${issue.id} is review_gate: required — an app cannot set it to done; approval is only possible from the Specs view`
277
+ : `${issue.id} is awaiting approval — its status can only be changed from the Specs view`;
278
+ logAppAudit({
279
+ appId: args.appId,
280
+ origin: args.origin,
281
+ method: 'pm.updateIssue.blocked',
282
+ decision: 'deny',
283
+ requestId: args.requestId,
284
+ spaceKey: args.spaceKey,
285
+ reason,
286
+ });
287
+ throw badRequest(reason);
288
+ }
268
289
  function assertYamlSafe(value, field, method, listItem = false) {
269
290
  if (/[\r\n]/.test(value))
270
291
  throw badRequest(`${method} ${field} must not contain line breaks`);
@@ -589,6 +610,7 @@ export function pmUpdateIssueHandler(args) {
589
610
  const issue = full.issues.find((candidate) => candidate.id === issueId);
590
611
  if (!issue)
591
612
  throw issueNotFound(boardId, issueId);
613
+ assertReviewGateNotBypassed(args, issue, fields);
592
614
  const fullPath = resolvePlanPath(args.workingDir, issue.path);
593
615
  if (!fullPath || !existsSync(fullPath))
594
616
  throw issueNotFound(boardId, issueId);
@@ -10,6 +10,8 @@ export const REQUEST_BUCKET_REFILL_PER_SECOND = 15;
10
10
  export const AI_ASSIST_BUCKET_CAPACITY = 3;
11
11
  export const AI_ASSIST_REFILL_PER_SECOND = 1 / 30;
12
12
  export const DEFAULT_INFERENCE_CONCURRENCY = 2;
13
+ export const CHAIN_WRITE_BUCKET_CAPACITY = 10;
14
+ export const CHAIN_WRITE_REFILL_PER_SECOND = 1 / 6;
13
15
  function ledgerKey(appId, spaceKey) {
14
16
  return `${appId}\u0000${spaceKey}`;
15
17
  }
@@ -20,12 +22,14 @@ const pendingStarts = new Map();
20
22
  const pendingInferenceStarts = new Map();
21
23
  const requestBuckets = new Map();
22
24
  const aiAssistBuckets = new Map();
25
+ const chainWriteBuckets = new Map();
23
26
  const sessionUsage = new Map();
24
27
  export function resetSdkRateLimitState() {
25
28
  pendingStarts.clear();
26
29
  pendingInferenceStarts.clear();
27
30
  requestBuckets.clear();
28
31
  aiAssistBuckets.clear();
32
+ chainWriteBuckets.clear();
29
33
  sessionUsage.clear();
30
34
  }
31
35
  function intKnob(value, fallback) {
@@ -79,6 +83,25 @@ function throttleAiAssist(appId, nowMs) {
79
83
  bucket.tokens -= 1;
80
84
  return false;
81
85
  }
86
+ function throttleChainWrite(appId, nowMs) {
87
+ let bucket = chainWriteBuckets.get(appId);
88
+ if (!bucket) {
89
+ bucket = { tokens: CHAIN_WRITE_BUCKET_CAPACITY, lastRefillMs: nowMs };
90
+ chainWriteBuckets.set(appId, bucket);
91
+ }
92
+ const elapsedMs = Math.max(0, nowMs - bucket.lastRefillMs);
93
+ bucket.tokens = Math.min(CHAIN_WRITE_BUCKET_CAPACITY, bucket.tokens + (elapsedMs / 1000) * CHAIN_WRITE_REFILL_PER_SECOND);
94
+ bucket.lastRefillMs = nowMs;
95
+ if (bucket.tokens < 1)
96
+ return true;
97
+ bucket.tokens -= 1;
98
+ return false;
99
+ }
100
+ export const CHAIN_WRITE_METHODS = new Set([
101
+ 'pm.chains.create',
102
+ 'pm.chains.update',
103
+ 'pm.chains.delete',
104
+ ]);
82
105
  const ENGINE_RUN_METHODS = new Set([
83
106
  'agents.run',
84
107
  'browser.automate',
@@ -183,6 +206,15 @@ export function checkSdkRateLimit(op, now = Date.now) {
183
206
  }
184
207
  return null;
185
208
  }
209
+ if (CHAIN_WRITE_METHODS.has(op.method)) {
210
+ if (throttleChainWrite(op.appId, now())) {
211
+ return {
212
+ limited: true,
213
+ message: `pm.chains write rate limit reached (burst ${CHAIN_WRITE_BUCKET_CAPACITY}, then one every 6s); retry shortly`,
214
+ };
215
+ }
216
+ return null;
217
+ }
186
218
  if (throttleRequest(op.appId, now())) {
187
219
  return {
188
220
  limited: true,
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2025-present Minionry, Inc.
2
2
  // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
3
  import { createHash } from 'node:crypto';
4
+ import { agentSchedulesCreateHandler, agentSchedulesDeleteHandler, agentSchedulesListHandler, agentSchedulesRunNowHandler, agentSchedulesUpdateHandler, resolveAgentSchedulesScopes, } from './agent-schedule.js';
4
5
  import { agentsSessionsAnswerQuestionHandler, agentsSessionsApproveHandler, agentsSessionsAttachHandler, agentsSessionsCloseHandler, agentsSessionsCreateHandler, agentsSessionsHistoryHandler, agentsSessionsListHandler, agentsSessionsPromptHandler, agentsSessionsRenameHandler, agentsSessionsReplayHandler, agentsSessionsStopHandler, } from './agent-sessions.js';
5
6
  import { agentsResultHandler, agentsRunAdmit, agentsRunBouncerInput, agentsRunHandler, resolveAgentsRunScopes, sdkCancelHandler, } from './agents.js';
6
7
  import { toolsRegisterHandler, toolsResultHandler } from './app-tools.js';
@@ -16,8 +17,9 @@ import { auditRecentHandler, listGrantsHandler } from './host-ops.js';
16
17
  import { inferenceBouncerInput, inferenceChatHandler, inferenceResultHandler } from './inference.js';
17
18
  import { getLiveSubscriptionsForApp } from './live-registry.js';
18
19
  import { pmCreateBoardHandler, pmCreateIssueHandler, pmDeleteIssueHandler, pmGetBoardHandler, pmListBoardsHandler, pmOnBoardUpdateHandler, pmUpdateBoardHandler, pmUpdateIssueHandler, } from './pm.js';
20
+ import { pmChainsCreateHandler, pmChainsDeleteHandler, pmChainsListHandler, pmChainsUpdateHandler, } from './pm-chain.js';
19
21
  import { pmExecuteHandler, pmPauseHandler, pmResumeHandler, pmStopHandler } from './pm-execute.js';
20
- import { pmSchedulesCreateHandler, pmSchedulesDeleteHandler, pmSchedulesListHandler, pmSchedulesUpdateHandler, } from './pm-schedule.js';
22
+ import { pmSchedulesCreateHandler, pmSchedulesDeleteHandler, pmSchedulesListHandler, pmSchedulesRunNowHandler, pmSchedulesUpdateHandler, resolvePmSchedulesCreateScopes, } from './pm-schedule.js';
21
23
  import { qualityCancelHandler, qualityDetectToolsHandler, qualityInstallToolsHandler, qualityResultHandler, qualityRunHandler, qualitySaveDirectoriesHandler, qualitySetFindingStateHandler, qualitySetSettingsHandler, qualitySettingsHandler, qualityStateHandler, qualitySubscribeChangesHandler, } from './quality.js';
22
24
  import { qualitySchedulesCreateHandler, qualitySchedulesDeleteHandler, qualitySchedulesListHandler, qualitySchedulesUpdateHandler, } from './quality-schedule.js';
23
25
  import { dropSubscription, revokeGrantsHandler } from './revoke.js';
@@ -29,6 +31,7 @@ export const SDK_WRITE_CAPABILITY = {
29
31
  sdkPmWrite: 'editPlan',
30
32
  sdkPmExecute: 'editPlan',
31
33
  sdkPmSchedule: 'editPlan',
34
+ sdkPmChain: 'editPlan',
32
35
  sdkFilesWrite: 'editFiles',
33
36
  sdkStorageWrite: 'editFiles',
34
37
  sdkCancel: 'execute',
@@ -85,7 +88,17 @@ function sdkFeaturesHandler() {
85
88
  'agentsAppEndpoint',
86
89
  'qualityFindingState',
87
90
  'agentsModelSelection',
91
+ 'filesChangeKinds',
92
+ 'filesWatch',
93
+ 'filesConditionalWrite',
94
+ 'inferenceFastMode',
88
95
  'qualitySchedules',
96
+ 'pmChains',
97
+ 'agentsEngineSelection',
98
+ 'agentSchedules',
99
+ 'scheduleWallClock',
100
+ 'pmScheduleZip',
101
+ 'pmScheduleRunNow',
89
102
  ];
90
103
  }
91
104
  function notificationsUnsupportedHandler() {
@@ -264,6 +277,8 @@ export const SDK_METHOD_REGISTRY = {
264
277
  'pm.listBoards': { scopes: ['pm:read'], type: 'sdkRequest', handler: pmListBoardsHandler },
265
278
  'pm.subscribeBoard': { scopes: ['pm:read'], type: 'sdkRequest', handler: pmOnBoardUpdateHandler },
266
279
  'pm.schedules.list': { scopes: ['pm:schedule'], type: 'sdkRequest', handler: pmSchedulesListHandler },
280
+ 'pm.chains.list': { scopes: ['pm:chain'], type: 'sdkRequest', handler: pmChainsListHandler },
281
+ 'agents.schedules.list': { scopes: ['agents:schedule'], type: 'sdkRequest', handler: agentSchedulesListHandler },
267
282
  'agents.result': { scopes: ['agents:run'], type: 'sdkRequest', handler: agentsResultHandler },
268
283
  'inference.result': { scopes: ['inference:run'], type: 'sdkRequest', handler: inferenceResultHandler },
269
284
  'endpoints.list': { scopes: ['endpoints:register'], type: 'sdkRequest', handler: endpointsListHandler },
@@ -367,9 +382,18 @@ export const SDK_METHOD_REGISTRY = {
367
382
  'pm.pause': { scopes: ['pm:execute'], type: 'sdkPmExecute', handler: pmPauseHandler },
368
383
  'pm.resume': { scopes: ['pm:execute'], type: 'sdkPmExecute', handler: pmResumeHandler },
369
384
  'pm.stop': { scopes: ['pm:execute'], type: 'sdkPmExecute', handler: pmStopHandler },
370
- 'pm.schedules.create': { scopes: ['pm:schedule'], type: 'sdkPmSchedule', handler: pmSchedulesCreateHandler },
385
+ 'pm.schedules.create': {
386
+ scopes: ['pm:schedule'],
387
+ resolveScopes: resolvePmSchedulesCreateScopes,
388
+ type: 'sdkPmSchedule',
389
+ handler: pmSchedulesCreateHandler,
390
+ },
371
391
  'pm.schedules.update': { scopes: ['pm:schedule'], type: 'sdkPmSchedule', handler: pmSchedulesUpdateHandler },
372
392
  'pm.schedules.delete': { scopes: ['pm:schedule'], type: 'sdkPmSchedule', handler: pmSchedulesDeleteHandler },
393
+ 'pm.schedules.runNow': { scopes: ['pm:schedule'], type: 'sdkPmSchedule', handler: pmSchedulesRunNowHandler },
394
+ 'pm.chains.create': { scopes: ['pm:chain'], type: 'sdkPmChain', handler: pmChainsCreateHandler },
395
+ 'pm.chains.update': { scopes: ['pm:chain'], type: 'sdkPmChain', handler: pmChainsUpdateHandler },
396
+ 'pm.chains.delete': { scopes: ['pm:chain'], type: 'sdkPmChain', handler: pmChainsDeleteHandler },
373
397
  'files.write': {
374
398
  scopes: ['files:app'],
375
399
  resolveScopes: resolveFilesWriteScopes,
@@ -415,6 +439,20 @@ export const SDK_METHOD_REGISTRY = {
415
439
  },
416
440
  'agents.sessions.prompt': { scopes: ['agents:sessions'], type: 'sdkAgentInteract', handler: agentsSessionsPromptHandler },
417
441
  'agents.sessions.stop': { scopes: ['agents:sessions'], type: 'sdkAgentInteract', handler: agentsSessionsStopHandler },
442
+ 'agents.schedules.create': {
443
+ scopes: ['agents:schedule'],
444
+ resolveScopes: resolveAgentSchedulesScopes,
445
+ type: 'sdkAgentSession',
446
+ handler: agentSchedulesCreateHandler,
447
+ },
448
+ 'agents.schedules.update': {
449
+ scopes: ['agents:schedule'],
450
+ resolveScopes: resolveAgentSchedulesScopes,
451
+ type: 'sdkAgentSession',
452
+ handler: agentSchedulesUpdateHandler,
453
+ },
454
+ 'agents.schedules.delete': { scopes: ['agents:schedule'], type: 'sdkAgentSession', handler: agentSchedulesDeleteHandler },
455
+ 'agents.schedules.runNow': { scopes: ['agents:schedule'], type: 'sdkAgentSession', handler: agentSchedulesRunNowHandler },
418
456
  'git.stage': { scopes: ['git:write'], type: 'sdkGitWrite', handler: gitStageHandler, bouncerToolName: 'git_stage' },
419
457
  'git.unstage': { scopes: ['git:write'], type: 'sdkGitWrite', handler: gitUnstageHandler, bouncerToolName: 'git_unstage' },
420
458
  'git.commit': { scopes: ['git:write'], type: 'sdkGitWrite', handler: gitCommitHandler, bouncerToolName: 'git_commit' },
@@ -149,7 +149,7 @@ export async function terminalOpenHandler(args) {
149
149
  origin: args.origin,
150
150
  spaceKey: args.spaceKey,
151
151
  kind: 'terminal.open',
152
- viewerId: getGrant(args.appId, args.spaceKey) ? undefined : args.viewerId,
152
+ viewerId: args.viewerId,
153
153
  teardown: () => {
154
154
  unsubscribeRoster();
155
155
  if (isSdkOpenedTerminalId(opened.id))
@@ -4,6 +4,8 @@ import { existsSync, readFileSync } from 'node:fs';
4
4
  import { createStackParser, createTransport, initAndBind, nodeStackLineParser, ServerRuntimeClient, captureException as sentryCaptureException, captureMessage as sentryCaptureMessage, flush as sentryFlush, } from '@sentry/core';
5
5
  import { machineFile } from '../../bin/machine-layout.js';
6
6
  import { resolvePlatformUrl } from './platform-url.js';
7
+ import { relayHttp } from './relay-http-client.js';
8
+ const CONFIG_FETCH_BUDGET_MS = 5_000;
7
9
  const SENTRY_TUNNEL_DSN = 'https://tunnel@sentry.io/0';
8
10
  const PLATFORM_URL = resolvePlatformUrl();
9
11
  let initialized = false;
@@ -27,7 +29,9 @@ function isTelemetryEnabled() {
27
29
  }
28
30
  async function fetchSentryEnabled() {
29
31
  try {
30
- const response = await fetch(`${PLATFORM_URL}/api/config/client`);
32
+ const response = await relayHttp.fetch(`${PLATFORM_URL}/api/config/client`, undefined, {
33
+ budgetMs: CONFIG_FETCH_BUDGET_MS,
34
+ });
31
35
  if (!response.ok)
32
36
  return false;
33
37
  const data = await response.json();