@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.
- package/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -8,6 +8,23 @@ import { PlanWatcher } from '../plan/watcher.js';
|
|
|
8
8
|
export const watcherCache = new Map();
|
|
9
9
|
export const executorCache = new Map();
|
|
10
10
|
export const schedulerCache = new Map();
|
|
11
|
+
export const chainEngineCache = new Map();
|
|
12
|
+
export function notifyChainRunEnded(workingDir, boardId, signal) {
|
|
13
|
+
try {
|
|
14
|
+
chainEngineCache.get(workingDir)?.notifyRunEnded(boardId, signal);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
console.error('[chain] notifyRunEnded failed:', error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function notifyChainExecutorStatus(workingDir, status) {
|
|
21
|
+
try {
|
|
22
|
+
chainEngineCache.get(workingDir)?.notifyExecutorStatus(status);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error('[chain] notifyExecutorStatus failed:', error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
11
28
|
export function resolvePlanPath(workingDir, relativePath) {
|
|
12
29
|
const pmDir = resolvePmDir(workingDir);
|
|
13
30
|
if (!pmDir)
|
|
@@ -68,6 +85,7 @@ labels: []
|
|
|
68
85
|
| todo | ready | Refined and queued for an agent |
|
|
69
86
|
| in_progress | started | An agent is working it now |
|
|
70
87
|
| in_review | started | Agent finished; awaiting the AI review gate |
|
|
88
|
+
| awaiting_approval | started | Passed AI review; waiting for a person to approve |
|
|
71
89
|
| done | completed | Passed review |
|
|
72
90
|
| cancelled | cancelled | Will not be done |
|
|
73
91
|
`;
|
|
@@ -128,6 +146,11 @@ export function cleanupPlanResources(workingDir) {
|
|
|
128
146
|
scheduler.stop();
|
|
129
147
|
schedulerCache.delete(workingDir);
|
|
130
148
|
}
|
|
149
|
+
const chainEngine = chainEngineCache.get(workingDir);
|
|
150
|
+
if (chainEngine) {
|
|
151
|
+
chainEngine.stop();
|
|
152
|
+
chainEngineCache.delete(workingDir);
|
|
153
|
+
}
|
|
131
154
|
}
|
|
132
155
|
export function scaffoldPmDirectory(workingDir, name) {
|
|
133
156
|
const planDir = defaultPmDir(workingDir);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { basename } from 'node:path';
|
|
4
|
+
import { basename, relative, sep } from 'node:path';
|
|
5
|
+
import { resolveBoardDir, tryCompleteBoardIfDone } from '../plan/board-config.js';
|
|
5
6
|
import { getPlanningStatus } from '../plan/composer.js';
|
|
6
7
|
import { checkAllAcceptanceCriteria, replaceFrontMatterField } from '../plan/front-matter.js';
|
|
7
|
-
import { parsePlanDirectory, parseSingleIssue, parseSingleMilestone, parseSingleSprint, planDirExists } from '../plan/parser.js';
|
|
8
|
+
import { NO_SPECS_DIR_ERROR, parseBoardDirectory, parsePlanDirectory, parseSingleIssue, parseSingleMilestone, parseSingleSprint, planDirExists, resolvePmDir } from '../plan/parser.js';
|
|
9
|
+
import { latestReviewVerdict, writeReviewApproval } from '../plan/review-approval.js';
|
|
8
10
|
import { tryCompleteParentEpic } from '../plan/state-reconciler.js';
|
|
9
11
|
import { hasScopeBinding, setScopeRepo, setScopeWorktree } from './handler-context.js';
|
|
10
|
-
import {
|
|
12
|
+
import { maybeContinueAfterApproval } from './plan-execution-handlers.js';
|
|
13
|
+
import { denyIfViewOnly, executorCache, formatYamlValue, getWatcher, notifyChainRunEnded, resolvePlanPath, scaffoldPmDirectory } from './plan-helpers.js';
|
|
11
14
|
function restoreBoardScopes(ctx, boardWorktrees) {
|
|
12
15
|
for (const [boardId, entry] of Object.entries(boardWorktrees)) {
|
|
13
16
|
if (hasScopeBinding(ctx, boardId))
|
|
@@ -95,7 +98,35 @@ export function handleGetMilestone(ctx, ws, msg, workingDir) {
|
|
|
95
98
|
}
|
|
96
99
|
ctx.send(ws, { type: 'planMilestone', data: milestone });
|
|
97
100
|
}
|
|
98
|
-
|
|
101
|
+
function recordStatusOverrideIfParked(workingDir, path, patchedStatus, decidedBy) {
|
|
102
|
+
const current = typeof patchedStatus === 'string' ? parseSingleIssue(workingDir, path) : null;
|
|
103
|
+
if (!current || current.reviewGate !== 'required' || current.status !== 'awaiting_approval')
|
|
104
|
+
return null;
|
|
105
|
+
const pmDir = resolvePmDir(workingDir);
|
|
106
|
+
const boardId = current.path.match(/^boards\/([^/]+)\//)?.[1];
|
|
107
|
+
const boardDir = pmDir && boardId ? resolveBoardDir(pmDir, boardId) : null;
|
|
108
|
+
if (!boardDir)
|
|
109
|
+
return `Could not resolve the board for ${current.id} — nothing was changed`;
|
|
110
|
+
const decidedAt = new Date();
|
|
111
|
+
const record = {
|
|
112
|
+
issueId: current.id,
|
|
113
|
+
decision: patchedStatus === 'done' ? 'approve' : 'reject',
|
|
114
|
+
comment: null,
|
|
115
|
+
decidedAt: decidedAt.toISOString(),
|
|
116
|
+
decidedBy,
|
|
117
|
+
via: 'status_override',
|
|
118
|
+
verdictFile: latestReviewVerdict(boardDir, current.id)?.fileName ?? null,
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
writeReviewApproval(boardDir, record, decidedAt.getTime());
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return `Could not record the decision for ${current.id}: ${errorText(error)} — nothing was changed`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function handleUpdateIssue(ctx, ws, msg, workingDir, permission,
|
|
129
|
+
decidedBy = { role: 'owner' }) {
|
|
99
130
|
if (denyIfViewOnly(ctx, ws, permission))
|
|
100
131
|
return;
|
|
101
132
|
const { path, fields } = msg.data || {};
|
|
@@ -108,6 +139,11 @@ export function handleUpdateIssue(ctx, ws, msg, workingDir, permission) {
|
|
|
108
139
|
ctx.send(ws, { type: 'planError', data: { error: `File not found: ${path}` } });
|
|
109
140
|
return;
|
|
110
141
|
}
|
|
142
|
+
const statusOverrideError = recordStatusOverrideIfParked(workingDir, path, fields.status, decidedBy);
|
|
143
|
+
if (statusOverrideError) {
|
|
144
|
+
ctx.send(ws, { type: 'planError', data: { error: statusOverrideError } });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
111
147
|
if (!applyIssueFieldUpdates(fullPath, fields)) {
|
|
112
148
|
ctx.send(ws, { type: 'planError', data: { error: 'Invalid file format' } });
|
|
113
149
|
return;
|
|
@@ -166,3 +202,124 @@ export function handleScaffold(ctx, ws, msg, workingDir, permission) {
|
|
|
166
202
|
const fullState = parsePlanDirectory(workingDir);
|
|
167
203
|
ctx.broadcastToAll({ type: 'planScaffolded', data: fullState });
|
|
168
204
|
}
|
|
205
|
+
export function handleReviewDecision(ctx, ws, msg, workingDir, permission,
|
|
206
|
+
decidedBy = { role: 'owner' }) {
|
|
207
|
+
if (denyIfViewOnly(ctx, ws, permission))
|
|
208
|
+
return;
|
|
209
|
+
const request = parseReviewDecisionRequest(msg.data);
|
|
210
|
+
if (typeof request === 'string') {
|
|
211
|
+
ctx.send(ws, { type: 'planError', data: { error: request } });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const target = resolveReviewDecisionTarget(workingDir, request);
|
|
215
|
+
if (typeof target === 'string') {
|
|
216
|
+
ctx.send(ws, { type: 'planError', data: { error: target } });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const decidedAt = new Date();
|
|
220
|
+
const record = {
|
|
221
|
+
issueId: target.issue.id,
|
|
222
|
+
decision: request.decision,
|
|
223
|
+
comment: request.comment,
|
|
224
|
+
decidedAt: decidedAt.toISOString(),
|
|
225
|
+
decidedBy,
|
|
226
|
+
via: 'review_decision',
|
|
227
|
+
verdictFile: latestReviewVerdict(target.boardDir, target.issue.id)?.fileName ?? null,
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
writeReviewApproval(target.boardDir, record, decidedAt.getTime());
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
ctx.send(ws, {
|
|
234
|
+
type: 'planError',
|
|
235
|
+
data: { error: `Could not record the decision for ${target.issue.id}: ${errorText(error)} — nothing was changed` },
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!applyIssueFieldUpdates(target.fullPath, { status: request.decision === 'approve' ? 'done' : 'todo' })) {
|
|
240
|
+
ctx.send(ws, { type: 'planError', data: { error: `Invalid file format: ${target.relPath}` } });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (request.decision === 'reject' && request.comment) {
|
|
244
|
+
appendChangesRequested(target.fullPath, request.comment, decidedAt);
|
|
245
|
+
}
|
|
246
|
+
void settleReviewDecision(ctx, workingDir, request, target).catch((error) => {
|
|
247
|
+
ctx.send(ws, {
|
|
248
|
+
type: 'planError',
|
|
249
|
+
data: { error: `${target.issue.id} was ${request.decision === 'approve' ? 'approved' : 'sent back'} but the board could not be reconciled: ${errorText(error)}` },
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function parseReviewDecisionRequest(data) {
|
|
254
|
+
const { boardId, path, decision, comment } = (data ?? {});
|
|
255
|
+
if (typeof boardId !== 'string' || boardId.length === 0)
|
|
256
|
+
return 'Board id required';
|
|
257
|
+
if (typeof path !== 'string' || path.length === 0)
|
|
258
|
+
return 'Issue path required';
|
|
259
|
+
if (decision !== 'approve' && decision !== 'reject')
|
|
260
|
+
return "Decision must be 'approve' or 'reject'";
|
|
261
|
+
const note = typeof comment === 'string' ? comment.trim() : '';
|
|
262
|
+
if (decision === 'reject' && note.length === 0)
|
|
263
|
+
return 'A comment is required when requesting changes';
|
|
264
|
+
return { boardId, path, decision, comment: note.length > 0 ? note : null };
|
|
265
|
+
}
|
|
266
|
+
function resolveReviewDecisionTarget(workingDir, request) {
|
|
267
|
+
const pmDir = resolvePmDir(workingDir);
|
|
268
|
+
if (!pmDir)
|
|
269
|
+
return NO_SPECS_DIR_ERROR;
|
|
270
|
+
const boardDir = resolveBoardDir(pmDir, request.boardId);
|
|
271
|
+
if (!boardDir)
|
|
272
|
+
return `Board not found: ${request.boardId}`;
|
|
273
|
+
const fullPath = resolvePlanPath(workingDir, request.path);
|
|
274
|
+
if (!fullPath || !isInsideDir(fullPath, boardDir)) {
|
|
275
|
+
return `Issue ${request.path} is not on board ${request.boardId}`;
|
|
276
|
+
}
|
|
277
|
+
const relPath = relative(pmDir, fullPath);
|
|
278
|
+
const issue = existsSync(fullPath) ? parseSingleIssue(workingDir, relPath) : null;
|
|
279
|
+
if (!issue)
|
|
280
|
+
return `Issue not found: ${relPath}`;
|
|
281
|
+
if (issue.reviewGate !== 'required') {
|
|
282
|
+
return `${issue.id} has no approval gate — its review_gate is '${issue.reviewGate}'`;
|
|
283
|
+
}
|
|
284
|
+
if (issue.status !== 'awaiting_approval') {
|
|
285
|
+
return `${issue.id} is not awaiting approval (status: ${issue.status})`;
|
|
286
|
+
}
|
|
287
|
+
return { pmDir, boardDir, fullPath, relPath, issue };
|
|
288
|
+
}
|
|
289
|
+
function isInsideDir(candidate, dir) {
|
|
290
|
+
return candidate.startsWith(`${dir}${sep}`);
|
|
291
|
+
}
|
|
292
|
+
async function settleReviewDecision(ctx, workingDir, request, target) {
|
|
293
|
+
const updated = parseSingleIssue(workingDir, target.relPath);
|
|
294
|
+
let epic = null;
|
|
295
|
+
if (request.decision === 'approve' && updated) {
|
|
296
|
+
const epicPath = tryCompleteParentEpic(workingDir, updated);
|
|
297
|
+
epic = epicPath ? parseSingleIssue(workingDir, epicPath) : null;
|
|
298
|
+
await tryCompleteBoardIfDone(target.pmDir, request.boardId, parseBoardDirectory(target.pmDir, request.boardId)?.issues ?? [], (message) => { console.warn(`[plan-review-decision] ${message}`); });
|
|
299
|
+
notifyChainRunEnded(workingDir, request.boardId, 'complete');
|
|
300
|
+
}
|
|
301
|
+
ctx.broadcastToAll({ type: 'planIssueUpdated', data: updated });
|
|
302
|
+
if (epic)
|
|
303
|
+
ctx.broadcastToAll({ type: 'planIssueUpdated', data: epic });
|
|
304
|
+
const fullState = parsePlanDirectory(workingDir);
|
|
305
|
+
if (fullState)
|
|
306
|
+
ctx.broadcastToAll({ type: 'planStateUpdated', data: fullState });
|
|
307
|
+
if (request.decision === 'approve')
|
|
308
|
+
maybeContinueAfterApproval(ctx, workingDir, request.boardId);
|
|
309
|
+
}
|
|
310
|
+
const ACTIVITY_HEADING_RE = /^## Activity[^\S\r\n]*$/m;
|
|
311
|
+
function appendChangesRequested(fullPath, comment, decidedAt) {
|
|
312
|
+
try {
|
|
313
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
314
|
+
const entry = `- Changes requested (${decidedAt.toISOString().split('T')[0]}): ${comment.replace(/\s*\n\s*/g, ' ')}`;
|
|
315
|
+
const heading = ACTIVITY_HEADING_RE.exec(content);
|
|
316
|
+
const cut = heading?.index === undefined ? -1 : heading.index + heading[0].length;
|
|
317
|
+
writeFileSync(fullPath, cut < 0
|
|
318
|
+
? `${content}\n\n## Activity\n${entry}`
|
|
319
|
+
: `${content.slice(0, cut)}\n${entry}${content.slice(cut)}`, 'utf-8');
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
}
|
|
323
|
+
function errorText(error) {
|
|
324
|
+
return error instanceof Error ? error.message : String(error);
|
|
325
|
+
}
|
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { replaceFrontMatterField } from '../plan/front-matter.js';
|
|
6
6
|
import { defaultPmDir, getNextSprintId, parsePlanDirectory, parseSingleSprint, parseSprintArtifacts, resolvePmDir } from '../plan/parser.js';
|
|
7
|
+
import { broadcastPlanStateIfChanged } from '../plan/watcher.js';
|
|
7
8
|
import { denyIfViewOnly, resolvePlanPath } from './plan-helpers.js';
|
|
8
9
|
function buildSprintMarkdown(id, title, goal, start, end, issueRefs) {
|
|
9
10
|
const issuesYaml = issueRefs.length > 0
|
|
@@ -114,7 +115,7 @@ export function handleActivateSprint(ctx, ws, msg, workingDir, permission) {
|
|
|
114
115
|
ctx.broadcastToAll({ type: 'planSprintUpdated', data: updatedSprint });
|
|
115
116
|
const updatedState = parsePlanDirectory(workingDir);
|
|
116
117
|
if (updatedState) {
|
|
117
|
-
ctx
|
|
118
|
+
broadcastPlanStateIfChanged(ctx, workingDir, updatedState);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
export function handleCompleteSprint(ctx, ws, msg, workingDir, permission) {
|
|
@@ -172,7 +173,7 @@ export function handleCompleteSprint(ctx, ws, msg, workingDir, permission) {
|
|
|
172
173
|
ctx.broadcastToAll({ type: 'planSprintCompleted', data: updatedSprint });
|
|
173
174
|
const updatedState = parsePlanDirectory(workingDir);
|
|
174
175
|
if (updatedState) {
|
|
175
|
-
ctx
|
|
176
|
+
broadcastPlanStateIfChanged(ctx, workingDir, updatedState);
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
export function handleGetSprintArtifacts(ctx, ws, msg, workingDir) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { can } from '@minionry/roles';
|
|
4
4
|
import { NO_SPECS_DIR_ERROR, resolvePmDir } from '../plan/parser.js';
|
|
5
|
-
import { cleanupScheduleSource, isValidTiming, parseCreateSourceWire, parseUpdatePatch, resolveCreateSource, } from '../schedule/schedule-wire.js';
|
|
5
|
+
import { cleanupScheduleSource, isValidTiming, parseCreateSourceWire, parseMaxLatenessMs, parseUpdatePatch, resolveCreateSource, } from '../schedule/schedule-wire.js';
|
|
6
6
|
import { getScheduler } from '../schedule/scheduler.js';
|
|
7
7
|
import { previewTemplateSource } from '../schedule/template-source.js';
|
|
8
8
|
import { dispatchByType } from './dispatch-util.js';
|
|
@@ -46,6 +46,11 @@ function handleScheduleCreate(ctx, ws, msg, workingDir, permission) {
|
|
|
46
46
|
reply({ success: false, error: 'Invalid timing' });
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
+
const maxLateness = parseMaxLatenessMs(data.maxLatenessMs);
|
|
50
|
+
if (!maxLateness.ok) {
|
|
51
|
+
reply({ success: false, error: 'Invalid maxLatenessMs (integer >= 60000)' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
49
54
|
const pmDir = resolvePmDir(workingDir);
|
|
50
55
|
if (!pmDir) {
|
|
51
56
|
reply({ success: false, error: NO_SPECS_DIR_ERROR });
|
|
@@ -70,6 +75,7 @@ function handleScheduleCreate(ctx, ws, msg, workingDir, permission) {
|
|
|
70
75
|
input: data.input,
|
|
71
76
|
timing: data.timing,
|
|
72
77
|
enabled: data.enabled,
|
|
78
|
+
maxLatenessMs: maxLateness.value,
|
|
73
79
|
});
|
|
74
80
|
notifySchedulerAndBroadcast(ctx, workingDir);
|
|
75
81
|
reply({
|
|
@@ -127,7 +133,7 @@ function handleScheduleUpdate(ctx, ws, msg, workingDir, permission) {
|
|
|
127
133
|
}
|
|
128
134
|
const safePatch = parseUpdatePatch(patch);
|
|
129
135
|
if (!safePatch) {
|
|
130
|
-
reply({ success: false, error: 'Invalid timing' });
|
|
136
|
+
reply({ success: false, error: 'Invalid timing or maxLatenessMs' });
|
|
131
137
|
return;
|
|
132
138
|
}
|
|
133
139
|
const store = getScheduleStore(ctx, workingDir);
|
|
@@ -78,8 +78,12 @@ function createAppAgentRun(ctx, start) {
|
|
|
78
78
|
}
|
|
79
79
|
: {}),
|
|
80
80
|
});
|
|
81
|
-
if (model)
|
|
81
|
+
if (model) {
|
|
82
82
|
session.applyOverrides({ engine: model.engine, model: model.value });
|
|
83
|
+
}
|
|
84
|
+
else if (selection) {
|
|
85
|
+
session.applyOverrides({ engine: selection.engine, ...(selection.model ? { model: selection.model } : {}) });
|
|
86
|
+
}
|
|
83
87
|
setupSessionListeners(ctx, session, APP_HOST_WS, tabId);
|
|
84
88
|
const sessionId = session.getSessionInfo().sessionId;
|
|
85
89
|
ctx.sessions.set(sessionId, session);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { getAgentScheduler } from '../schedule/agent-scheduler.js';
|
|
4
|
+
import { setSdkAgentScheduleHost } from '../sdk/agent-schedule.js';
|
|
5
|
+
export function installSdkAgentScheduleHost(ctx) {
|
|
6
|
+
const host = {
|
|
7
|
+
getStore(workingDir) {
|
|
8
|
+
const engine = getAgentScheduler(workingDir, ctx);
|
|
9
|
+
engine.start();
|
|
10
|
+
return engine.getStore();
|
|
11
|
+
},
|
|
12
|
+
notifyAndBroadcast(workingDir) {
|
|
13
|
+
const engine = getAgentScheduler(workingDir, ctx);
|
|
14
|
+
engine.tick();
|
|
15
|
+
const store = engine.getStore();
|
|
16
|
+
if (store)
|
|
17
|
+
ctx.broadcastToAll({ type: 'agentSchedulesChanged', data: { schedules: store.list() } });
|
|
18
|
+
},
|
|
19
|
+
fireNow(workingDir, scheduleId) {
|
|
20
|
+
const engine = getAgentScheduler(workingDir, ctx);
|
|
21
|
+
engine.start();
|
|
22
|
+
const result = engine.fireNow(scheduleId);
|
|
23
|
+
const store = engine.getStore();
|
|
24
|
+
if (store)
|
|
25
|
+
ctx.broadcastToAll({ type: 'agentSchedulesChanged', data: { schedules: store.list() } });
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
setSdkAgentScheduleHost(host);
|
|
30
|
+
}
|
|
@@ -23,7 +23,7 @@ export function installSdkBrowserHost(ctx) {
|
|
|
23
23
|
navigate: async ({ url, tabId }) => {
|
|
24
24
|
await (await ensureReady(ctx)).navigate(url, tabId);
|
|
25
25
|
},
|
|
26
|
-
open: async ({ url, reuseOrigin }) => {
|
|
26
|
+
open: async ({ url, reuseOrigin, opener }) => {
|
|
27
27
|
const live = await ensureReady(ctx);
|
|
28
28
|
if (reuseOrigin) {
|
|
29
29
|
const match = live.getTabsSnapshot().tabs.find((t) => sameOrigin(t.url, reuseOrigin));
|
|
@@ -32,7 +32,7 @@ export function installSdkBrowserHost(ctx) {
|
|
|
32
32
|
return { tabId: match.id };
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
const tab = await live.openTab(url);
|
|
35
|
+
const tab = await live.openTab(url, { kind: 'app', appId: opener.appId, origin: opener.origin });
|
|
36
36
|
return { tabId: tab.id };
|
|
37
37
|
},
|
|
38
38
|
close: async ({ tabId }) => {
|
|
@@ -75,7 +75,8 @@ export function installSdkBrowserHost(ctx) {
|
|
|
75
75
|
onEvent(event);
|
|
76
76
|
},
|
|
77
77
|
broadcastToAll: (response) => ctx.broadcastToAll(response),
|
|
78
|
-
}
|
|
78
|
+
},
|
|
79
|
+
initiator ? { kind: 'app', appId: initiator.appId, origin: initiator.origin } : undefined);
|
|
79
80
|
return {
|
|
80
81
|
agentId: run.agentId,
|
|
81
82
|
approve: (verdict) => resolveProposal(verdict),
|
|
@@ -130,11 +131,17 @@ async function runSdkPreflight(ctx) {
|
|
|
130
131
|
}
|
|
131
132
|
return toBrowserPreflightPayload(result, { chromiumInstalling: false });
|
|
132
133
|
}
|
|
134
|
+
function withoutOpener(tab) {
|
|
135
|
+
const { opener, ...sdkTab } = tab;
|
|
136
|
+
void opener;
|
|
137
|
+
return sdkTab;
|
|
138
|
+
}
|
|
133
139
|
function withOwners(payload) {
|
|
134
140
|
const driverState = getBrowserDriverState();
|
|
135
141
|
return {
|
|
136
142
|
currentTabId: payload.currentTabId,
|
|
137
|
-
tabs: payload.tabs.map((
|
|
143
|
+
tabs: payload.tabs.map((nativeTab) => {
|
|
144
|
+
const tab = withoutOpener(nativeTab);
|
|
138
145
|
const owner = driverState.getOwner(tab.id);
|
|
139
146
|
if (!owner)
|
|
140
147
|
return tab;
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parsePlanDirectory, resolvePmDir } from '../plan/parser.js';
|
|
5
|
+
import { broadcastPlanStateIfChanged } from '../plan/watcher.js';
|
|
5
6
|
import { injectGatherIssue, writeRunInputFile } from '../schedule/run-bootstrap.js';
|
|
6
7
|
import { resolveSdkConsent } from '../sdk/connect.js';
|
|
7
8
|
import { runSdkPipeline } from '../sdk/pipeline.js';
|
|
8
9
|
import { setSdkPmHost } from '../sdk/pm.js';
|
|
9
10
|
import { handleChatToBoard } from './plan-board-handlers.js';
|
|
10
11
|
import { executorCache, getWatcher } from './plan-helpers.js';
|
|
12
|
+
import { installSdkAgentScheduleHost } from './sdk-agent-schedule-host.js';
|
|
13
|
+
import { installSdkPmChainHost } from './sdk-pm-chain-host.js';
|
|
11
14
|
import { installSdkPmExecutionHost } from './sdk-pm-execution-host.js';
|
|
12
15
|
import { installSdkPmScheduleHost } from './sdk-pm-schedule-host.js';
|
|
13
16
|
import { installSdkQualityScheduleHost } from './sdk-quality-schedule-host.js';
|
|
@@ -116,7 +119,7 @@ function installSdkPmHost(ctx) {
|
|
|
116
119
|
getWatcher(workingDir, ctx).start();
|
|
117
120
|
const fullState = parsePlanDirectory(workingDir);
|
|
118
121
|
if (fullState)
|
|
119
|
-
ctx
|
|
122
|
+
broadcastPlanStateIfChanged(ctx, workingDir, fullState);
|
|
120
123
|
},
|
|
121
124
|
stopExecutorIfRunning: (workingDir, boardId) => {
|
|
122
125
|
const executor = executorCache.get(workingDir);
|
|
@@ -191,7 +194,9 @@ export async function handleSdkMessage(ctx, ws, msg, tabId, workingDir, role, vi
|
|
|
191
194
|
installSdkPmHost(ctx);
|
|
192
195
|
installSdkPmExecutionHost(ctx);
|
|
193
196
|
installSdkPmScheduleHost(ctx);
|
|
197
|
+
installSdkPmChainHost(ctx);
|
|
194
198
|
installSdkQualityScheduleHost();
|
|
199
|
+
installSdkAgentScheduleHost(ctx);
|
|
195
200
|
if (msg.type === 'sdkConsentDecision') {
|
|
196
201
|
handleSdkConsentDecision(msg, role, viewerId);
|
|
197
202
|
return;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { broadcastChainsChanged, getChainEngine } from '../chain/chain-engine.js';
|
|
4
|
+
import { setSdkPmChainHost } from '../sdk/pm-chain.js';
|
|
5
|
+
export function installSdkPmChainHost(ctx) {
|
|
6
|
+
const host = {
|
|
7
|
+
getStore(workingDir) {
|
|
8
|
+
const engine = getChainEngine(workingDir, ctx);
|
|
9
|
+
engine.start();
|
|
10
|
+
return engine.getStore();
|
|
11
|
+
},
|
|
12
|
+
notifyAndBroadcast(workingDir) {
|
|
13
|
+
const engine = getChainEngine(workingDir, ctx);
|
|
14
|
+
const store = engine.getStore();
|
|
15
|
+
if (store)
|
|
16
|
+
broadcastChainsChanged(ctx, store);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
setSdkPmChainHost(host);
|
|
20
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { setSdkPmExecutionHost } from '../sdk/pm-execute.js';
|
|
4
4
|
import { scopeExecutionDir } from './handler-context.js';
|
|
5
|
-
import { wireExecutorEvents } from './plan-execution-handlers.js';
|
|
5
|
+
import { broadcastBoardRunPrompt, wireExecutorEvents } from './plan-execution-handlers.js';
|
|
6
6
|
import { executorCache, getExecutor } from './plan-helpers.js';
|
|
7
7
|
const currentRunByWorkingDir = new Map();
|
|
8
8
|
function mintRunId() {
|
|
@@ -13,6 +13,25 @@ function clearCurrentRun(workingDir, runId) {
|
|
|
13
13
|
if (current?.runId === runId)
|
|
14
14
|
currentRunByWorkingDir.delete(workingDir);
|
|
15
15
|
}
|
|
16
|
+
export function startFreshBoardRun(ctx, workingDir, boardId, options) {
|
|
17
|
+
const executor = getExecutor(workingDir);
|
|
18
|
+
const runId = mintRunId();
|
|
19
|
+
currentRunByWorkingDir.set(workingDir, { runId, boardId });
|
|
20
|
+
const executionDir = scopeExecutionDir(ctx, boardId);
|
|
21
|
+
wireExecutorEvents(executor, ctx, workingDir, boardId, undefined, executionDir);
|
|
22
|
+
executor.on('complete', () => clearCurrentRun(workingDir, runId));
|
|
23
|
+
executor.on('error', () => clearCurrentRun(workingDir, runId));
|
|
24
|
+
if (options?.launchPrompt)
|
|
25
|
+
broadcastBoardRunPrompt(ctx, boardId, options.launchPrompt);
|
|
26
|
+
ctx.broadcastToAll({ type: 'planExecutionStarted', data: { status: 'executing', boardId } });
|
|
27
|
+
executor.startBoard(boardId, executionDir).catch((error) => {
|
|
28
|
+
ctx.broadcastToAll({
|
|
29
|
+
type: 'planExecutionError',
|
|
30
|
+
data: { error: error instanceof Error ? error.message : String(error), boardId },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return { runId };
|
|
34
|
+
}
|
|
16
35
|
export function installSdkPmExecutionHost(ctx) {
|
|
17
36
|
const host = {
|
|
18
37
|
attach(workingDir, boardId) {
|
|
@@ -28,19 +47,7 @@ export function installSdkPmExecutionHost(ctx) {
|
|
|
28
47
|
if (alreadyRunningThisBoard) {
|
|
29
48
|
return { runId: current.runId, on: (event, cb) => executor.on(event, cb), off: (event, cb) => executor.off(event, cb) };
|
|
30
49
|
}
|
|
31
|
-
const runId =
|
|
32
|
-
currentRunByWorkingDir.set(workingDir, { runId, boardId });
|
|
33
|
-
const executionDir = scopeExecutionDir(ctx, boardId);
|
|
34
|
-
wireExecutorEvents(executor, ctx, workingDir, boardId, undefined, executionDir);
|
|
35
|
-
executor.on('complete', () => clearCurrentRun(workingDir, runId));
|
|
36
|
-
executor.on('error', () => clearCurrentRun(workingDir, runId));
|
|
37
|
-
ctx.broadcastToAll({ type: 'planExecutionStarted', data: { status: 'executing', boardId } });
|
|
38
|
-
executor.startBoard(boardId, executionDir).catch((error) => {
|
|
39
|
-
ctx.broadcastToAll({
|
|
40
|
-
type: 'planExecutionError',
|
|
41
|
-
data: { error: error instanceof Error ? error.message : String(error), boardId },
|
|
42
|
-
});
|
|
43
|
-
});
|
|
50
|
+
const { runId } = startFreshBoardRun(ctx, workingDir, boardId);
|
|
44
51
|
return { runId, on: (event, cb) => executor.on(event, cb), off: (event, cb) => executor.off(event, cb) };
|
|
45
52
|
},
|
|
46
53
|
pause(workingDir, boardId) {
|
|
@@ -16,6 +16,15 @@ export function installSdkPmScheduleHost(ctx) {
|
|
|
16
16
|
if (store)
|
|
17
17
|
ctx.broadcastToAll({ type: 'planSchedulesChanged', data: { schedules: store.list() } });
|
|
18
18
|
},
|
|
19
|
+
fireNow(workingDir, scheduleId) {
|
|
20
|
+
const engine = getScheduler(workingDir, ctx);
|
|
21
|
+
engine.start();
|
|
22
|
+
const result = engine.fireNow(scheduleId);
|
|
23
|
+
const store = engine.getStore();
|
|
24
|
+
if (store)
|
|
25
|
+
ctx.broadcastToAll({ type: 'planSchedulesChanged', data: { schedules: store.list() } });
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
19
28
|
};
|
|
20
29
|
setSdkPmScheduleHost(host);
|
|
21
30
|
}
|
|
@@ -23,6 +23,7 @@ export function notifySdkTerminalSessionOpened(terminalId) {
|
|
|
23
23
|
}
|
|
24
24
|
export function resetSdkTerminalNotifications() {
|
|
25
25
|
sessionListeners.clear();
|
|
26
|
+
exitBroadcastCtx = null;
|
|
26
27
|
if (managerExitCleanup) {
|
|
27
28
|
managerExitCleanup();
|
|
28
29
|
managerExitCleanup = null;
|
|
@@ -44,17 +45,23 @@ function summarizeSession(id) {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
let managerExitCleanup = null;
|
|
48
|
+
let exitBroadcastCtx = null;
|
|
47
49
|
function wireManagerExitListener() {
|
|
48
50
|
if (managerExitCleanup)
|
|
49
51
|
return;
|
|
50
52
|
const ptyManager = getPTYManager();
|
|
51
53
|
const onExit = (terminalId) => {
|
|
54
|
+
const ownerLabel = ptyManager.getSessionInfo(terminalId)?.ownerLabel;
|
|
55
|
+
if (ownerLabel !== undefined) {
|
|
56
|
+
exitBroadcastCtx?.broadcastToAll({ type: 'terminalClosed', data: { terminalId, ownerLabel } });
|
|
57
|
+
}
|
|
52
58
|
notifySessionChange({ kind: 'closed', id: terminalId });
|
|
53
59
|
};
|
|
54
60
|
ptyManager.on('exit', onExit);
|
|
55
61
|
managerExitCleanup = () => ptyManager.off('exit', onExit);
|
|
56
62
|
}
|
|
57
63
|
export function installSdkTerminalHost(ctx) {
|
|
64
|
+
exitBroadcastCtx = ctx;
|
|
58
65
|
wireManagerExitListener();
|
|
59
66
|
const host = {
|
|
60
67
|
async ensurePtyAvailable() {
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import {
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { resolveClaudeCommand } from '../../engines/claude/claude-command.js';
|
|
4
|
+
import { resolveClaudeCommand, TOOL_LESS_CLAUDE_ARGS } from '../../engines/claude/claude-command.js';
|
|
7
5
|
import { computeClassifierHealth } from '../../mcp/classifier/health.js';
|
|
8
6
|
import { getModelCatalog } from '../model-catalog.js';
|
|
9
7
|
import { clearBouncerClassifierOverride, getCurrentSpaceIdentity, getSettings, setBouncerClassifier, setBouncerClassifierOverride, setEffortLevel, setEngine, setFastMode, setMaxConcurrentSessions, setMaxUploadSizeMb, setModel, setQualityEffortLevel, setQualityEngine, setQualityFastMode, setQualityModel, setSubagentModel, setSubagentModelForce, } from '../settings.js';
|
|
10
|
-
import { spacePath } from '../space-layout.js';
|
|
11
8
|
import { createSystemPrompt, deleteSystemPromptProfile, listSystemPromptProfiles, readSystemPromptFile, saveSystemPromptFile, systemPromptsDir, } from '../system-prompts.js';
|
|
12
9
|
import { isEngineId } from './types.js';
|
|
13
10
|
function buildSystemPromptProfilesMessage() {
|
|
@@ -173,10 +170,6 @@ export async function handleGetModelCatalog(ctx, ws, msg) {
|
|
|
173
170
|
}
|
|
174
171
|
export async function generateNotificationSummary(ctx, ws, tabId, userPrompt, output, workingDir) {
|
|
175
172
|
try {
|
|
176
|
-
const tempDir = spacePath(workingDir, 'tmp');
|
|
177
|
-
if (!existsSync(tempDir)) {
|
|
178
|
-
mkdirSync(tempDir, { recursive: true });
|
|
179
|
-
}
|
|
180
173
|
let truncatedOutput = output;
|
|
181
174
|
if (output.length > 4000) {
|
|
182
175
|
const firstPart = output.slice(0, 2000);
|
|
@@ -201,19 +194,19 @@ Generate a notification summary following these rules:
|
|
|
201
194
|
6. No emojis, no markdown, just plain text
|
|
202
195
|
|
|
203
196
|
Respond with ONLY the summary text, nothing else.`;
|
|
204
|
-
const promptFile = join(tempDir, `notif-summary-${Date.now()}.txt`);
|
|
205
|
-
writeFileSync(promptFile, summaryPrompt);
|
|
206
197
|
const systemPrompt = 'You are a notification summary assistant. Respond with only the summary text, no preamble or explanation.';
|
|
207
198
|
const args = [
|
|
208
199
|
'--print',
|
|
209
200
|
'--model', 'haiku',
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
...TOOL_LESS_CLAUDE_ARGS,
|
|
202
|
+
'--system-prompt', systemPrompt
|
|
212
203
|
];
|
|
213
204
|
const claude = spawn(resolveClaudeCommand(), args, {
|
|
214
205
|
cwd: workingDir,
|
|
215
|
-
stdio: ['
|
|
206
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
216
207
|
});
|
|
208
|
+
claude.stdin?.on('error', () => { });
|
|
209
|
+
claude.stdin?.end(summaryPrompt);
|
|
217
210
|
let stdout = '';
|
|
218
211
|
let stderr = '';
|
|
219
212
|
let responseSent = false;
|
|
@@ -234,11 +227,6 @@ Respond with ONLY the summary text, nothing else.`;
|
|
|
234
227
|
stderr += data.toString();
|
|
235
228
|
});
|
|
236
229
|
claude.on('close', (code) => {
|
|
237
|
-
try {
|
|
238
|
-
unlinkSync(promptFile);
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
}
|
|
242
230
|
if (code === 0 && stdout.trim()) {
|
|
243
231
|
sendSummaryOnce(stdout.trim().slice(0, 150));
|
|
244
232
|
}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { mirrorTabEventToSdk } from '../sdk/agents.js';
|
|
4
4
|
import { isTimelineTabEvent } from '../timeline/index.js';
|
|
5
|
+
import { isHighFrequencyTabEvent } from './viewer-interest.js';
|
|
5
6
|
export function broadcastTabEvent(ctx, tabId, type, data, engine) {
|
|
6
7
|
const buffer = ctx.tabEventBuffers.getOrCreate(tabId);
|
|
7
8
|
const seq = buffer.record(type, data);
|
|
8
9
|
const envelope = { type, tabId, data, seq };
|
|
9
10
|
if (engine)
|
|
10
11
|
envelope.engine = engine;
|
|
11
|
-
ctx.
|
|
12
|
+
if (isHighFrequencyTabEvent(type) && ctx.broadcastStream)
|
|
13
|
+
ctx.broadcastStream(envelope);
|
|
14
|
+
else
|
|
15
|
+
ctx.broadcastToAll(envelope);
|
|
12
16
|
mirrorTabEventToSdk(tabId, type, data, (response) => ctx.broadcastToAll(response));
|
|
13
17
|
assembleTabTimeline(ctx, tabId, type, data, engine);
|
|
14
18
|
return seq;
|