mstro-app 0.4.38 → 0.4.43
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/login.js +17 -7
- package/bin/commands/logout.js +14 -6
- package/bin/commands/status.js +9 -3
- package/bin/commands/whoami.js +10 -4
- package/bin/mstro.js +11 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts +1 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +2 -0
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
- package/dist/server/cli/headless/resilient-runner.js +234 -0
- package/dist/server/cli/headless/resilient-runner.js.map +1 -0
- package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
- package/dist/server/cli/headless/retry-strategies.js +262 -0
- package/dist/server/cli/headless/retry-strategies.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +5 -0
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +31 -4
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +1 -30
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +16 -3
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/prompt-builders.d.ts.map +1 -1
- package/dist/server/cli/prompt-builders.js +31 -13
- package/dist/server/cli/prompt-builders.js.map +1 -1
- package/dist/server/index.js +1 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +5 -4
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +1 -1
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +14 -8
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-patterns.js +1 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +19 -9
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +6 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +158 -76
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +1 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +6 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts +11 -0
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
- package/dist/server/services/plan/issue-classification.js +20 -0
- package/dist/server/services/plan/issue-classification.js.map +1 -0
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +10 -5
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +0 -5
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +12 -241
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +9 -6
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +11 -4
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +7 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +2 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +18 -7
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.js +90 -42
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +48 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +48 -1
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +74 -32
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +18 -18
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +52 -41
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/skill-watcher.js +85 -0
- package/dist/server/services/websocket/skill-watcher.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +2 -268
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +0 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-stream.ts +1 -0
- package/server/cli/headless/index.ts +2 -0
- package/server/cli/headless/resilient-runner.ts +354 -0
- package/server/cli/headless/retry-strategies.ts +330 -0
- package/server/cli/headless/stall-assessor.ts +5 -0
- package/server/cli/headless/tool-watchdog.ts +40 -4
- package/server/cli/improvisation-retry.ts +1 -32
- package/server/cli/improvisation-session-manager.ts +17 -3
- package/server/cli/prompt-builders.ts +33 -12
- package/server/index.ts +1 -9
- package/server/mcp/bouncer-cli.ts +5 -4
- package/server/mcp/bouncer-haiku.ts +1 -1
- package/server/mcp/bouncer-integration.ts +15 -8
- package/server/mcp/security-patterns.ts +1 -1
- package/server/services/plan/agents/code-review.md +109 -0
- package/server/services/plan/agents/commit-message.md +26 -0
- package/server/services/plan/agents/execute-issue.md +10 -1
- package/server/services/plan/agents/fix-quality.md +24 -0
- package/server/services/plan/agents/pr-description.md +28 -0
- package/server/services/plan/composer.ts +20 -9
- package/server/services/plan/executor.ts +160 -76
- package/server/services/plan/front-matter.ts +7 -0
- package/server/services/plan/issue-classification.ts +21 -0
- package/server/services/plan/issue-prompt-builder.ts +11 -5
- package/server/services/plan/issue-retry.ts +15 -330
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/review-gate.ts +9 -6
- package/server/services/plan/types.ts +3 -0
- package/server/services/platform-credentials.ts +10 -4
- package/server/services/terminal/pty-manager.ts +7 -1
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +18 -8
- package/server/services/websocket/plan-execution-handlers.ts +7 -7
- package/server/services/websocket/quality-fix-agent.ts +86 -44
- package/server/services/websocket/quality-handlers.ts +48 -7
- package/server/services/websocket/quality-persistence.ts +75 -1
- package/server/services/websocket/quality-review-agent.ts +70 -31
- package/server/services/websocket/quality-tools.ts +16 -14
- package/server/services/websocket/skill-handlers.ts +50 -40
- package/server/services/websocket/skill-watcher.ts +79 -0
- package/server/services/websocket/types.ts +0 -311
- package/dist/server/services/deploy/ai-broker.d.ts +0 -63
- package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
- package/dist/server/services/deploy/ai-broker.js +0 -360
- package/dist/server/services/deploy/ai-broker.js.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.js +0 -621
- package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
- package/dist/server/services/deploy/credentials.d.ts +0 -35
- package/dist/server/services/deploy/credentials.d.ts.map +0 -1
- package/dist/server/services/deploy/credentials.js +0 -177
- package/dist/server/services/deploy/credentials.js.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.js +0 -294
- package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.js +0 -266
- package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.js +0 -409
- package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
- package/server/cli/headless/RESEARCH.md +0 -627
- package/server/services/deploy/ai-broker.ts +0 -512
- package/server/services/deploy/board-execution-handler.ts +0 -847
- package/server/services/deploy/credentials.ts +0 -200
- package/server/services/deploy/deploy-ai-service.ts +0 -401
- package/server/services/deploy/headless-session-handler.ts +0 -414
- package/server/services/websocket/deploy-handlers.ts +0 -544
- package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
+
import { resolveIsCodeTask } from './issue-classification.js';
|
|
12
13
|
import type { Issue } from './types.js';
|
|
13
14
|
|
|
14
15
|
export interface IssuePromptOptions {
|
|
@@ -33,8 +34,11 @@ export function buildIssuePrompt(options: IssuePromptOptions): string {
|
|
|
33
34
|
.map(c => `- [${c.checked ? 'x' : ' '}] ${c.text}`)
|
|
34
35
|
.join('\n');
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
const isCode = resolveIsCodeTask(issue);
|
|
38
|
+
const codeFiles = isCode ? issue.filesToModify.filter(f => !f.match(/^Output:/i)) : [];
|
|
39
|
+
|
|
40
|
+
const files = codeFiles.length > 0
|
|
41
|
+
? `\n## Files to Modify\n${codeFiles.map(f => `- ${f}`).join('\n')}`
|
|
38
42
|
: '';
|
|
39
43
|
|
|
40
44
|
const predecessorDocs = resolvePredecessorDocs(issue, existingDocs);
|
|
@@ -69,15 +73,17 @@ ${files}${predecessorSection}
|
|
|
69
73
|
## Your Task
|
|
70
74
|
|
|
71
75
|
1. Read the full issue spec at ${pmDir ? join(pmDir, issue.path) : issue.path}
|
|
72
|
-
2.
|
|
76
|
+
${isCode ? `2. **Implement the code changes** in the source files listed under "Files to Modify". You MUST edit or create the actual source code files — the acceptance criteria describe what the code must do, not what to document. Read each target file first, then make the changes using Edit or Write.
|
|
77
|
+
3. After implementation, write a brief summary of what you changed to **${outputPath}**${predecessorDocs.length > 0 ? ' — this is the handoff artifact for downstream issues' : ''}
|
|
78
|
+
4. After writing output, update the issue front matter: change \`status: in_progress\` to \`status: in_review\`` : `2. Execute all acceptance criteria listed above
|
|
73
79
|
3. Write your output and results to **${outputPath}** — this is the handoff artifact for downstream issues
|
|
74
|
-
4. After writing output, update the issue front matter: change \`status: in_progress\` to \`status: in_review
|
|
80
|
+
4. After writing output, update the issue front matter: change \`status: in_progress\` to \`status: in_review\``}
|
|
75
81
|
|
|
76
82
|
## Rules
|
|
77
83
|
|
|
78
84
|
- Stay within this issue's scope. Do not modify files outside your assigned scope.
|
|
79
85
|
- The orchestrator manages STATE.md separately — do not edit STATE.md.
|
|
80
|
-
|
|
86
|
+
${isCode ? `- The output file is a summary of work done, NOT a substitute for implementation. You must modify the actual source code files listed in "Files to Modify". A review gate will verify the source files were changed.` : `- Write all significant output to ${outDir}/ so downstream issues can reference it.`}
|
|
81
87
|
- If you cannot complete the issue, leave status as \`in_progress\` and document what blocked you in the output file.`;
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -1,356 +1,41 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Brings the same resilience as Chat view (improvisation-retry.ts) to PM agents:
|
|
8
|
-
* - Tool timeout checkpoint recovery (preserves completed tools, skips hung tool)
|
|
9
|
-
* - Signal crash recovery (preserves accumulated results across retries)
|
|
10
|
-
* - Premature completion handling (max_tokens / end_turn → resume with "continue")
|
|
11
|
-
*
|
|
12
|
-
* Unlike Chat's retry system, PM agents don't maintain session continuity across
|
|
13
|
-
* prompts — each issue is independent — so we skip inter-movement recovery and
|
|
14
|
-
* simplify the resume strategy.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { hlog } from '../../cli/headless/headless-logger.js';
|
|
18
|
-
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
19
|
-
import { assessPrematureCompletion } from '../../cli/headless/stall-assessor.js';
|
|
20
|
-
import type { ExecutionCheckpoint, SessionResult } from '../../cli/headless/types.js';
|
|
21
|
-
import {
|
|
22
|
-
buildResumeRetryPrompt,
|
|
23
|
-
buildRetryPrompt,
|
|
24
|
-
buildSignalCrashRecoveryPrompt,
|
|
25
|
-
} from '../../cli/prompt-builders.js';
|
|
26
|
-
|
|
27
|
-
/** Max retries per issue execution (tool timeout, signal crash, premature completion combined) */
|
|
28
|
-
const MAX_ISSUE_RETRIES = 3;
|
|
29
|
-
|
|
30
|
-
/** Max accumulated tool results to carry across retries */
|
|
31
|
-
const MAX_ACCUMULATED_RESULTS = 50;
|
|
32
|
-
|
|
33
|
-
/** Lightweight tool record for accumulation across retries */
|
|
34
|
-
interface ToolRecord {
|
|
35
|
-
toolName: string;
|
|
36
|
-
toolId: string;
|
|
37
|
-
toolInput: Record<string, unknown>;
|
|
38
|
-
result?: string;
|
|
39
|
-
isError?: boolean;
|
|
40
|
-
duration?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface IssueRetryState {
|
|
44
|
-
currentPrompt: string;
|
|
45
|
-
retryNumber: number;
|
|
46
|
-
checkpoint: ExecutionCheckpoint | null;
|
|
47
|
-
accumulatedToolResults: ToolRecord[];
|
|
48
|
-
timedOutTools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>;
|
|
49
|
-
/** Session ID from a prior run — enables --resume for premature completion */
|
|
50
|
-
lastSessionId: string | undefined;
|
|
51
|
-
bestResult: SessionResult | null;
|
|
52
|
-
}
|
|
4
|
+
import { ResilientRunner } from '../../cli/headless/resilient-runner.js';
|
|
5
|
+
import type { SessionResult } from '../../cli/headless/types.js';
|
|
53
6
|
|
|
54
7
|
export interface IssueRunnerConfig {
|
|
55
8
|
workingDir: string;
|
|
56
|
-
/** Original enriched prompt for this issue */
|
|
57
9
|
prompt: string;
|
|
58
|
-
/** Stall detection timeouts (ms) */
|
|
59
10
|
stallWarningMs: number;
|
|
60
11
|
stallKillMs: number;
|
|
61
12
|
stallHardCapMs: number;
|
|
62
13
|
stallMaxExtensions: number;
|
|
63
|
-
/** Callback for streaming output to executor event bus */
|
|
64
14
|
outputCallback?: (text: string) => void;
|
|
65
|
-
/** Extra environment variables for spawned Claude processes (e.g. API keys) */
|
|
66
15
|
extraEnv?: Record<string, string>;
|
|
67
|
-
/** Signal to abort execution — when aborted, kills the running HeadlessRunner */
|
|
68
16
|
abortSignal?: AbortSignal;
|
|
69
17
|
}
|
|
70
18
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
* This wraps HeadlessRunner.run() with the same retry strategies as Chat view:
|
|
75
|
-
* 1. Tool timeout → checkpoint recovery with accumulated results
|
|
76
|
-
* 2. Signal crash → fresh start with preserved tool results
|
|
77
|
-
* 3. Premature completion → resume session with "continue"
|
|
78
|
-
*/
|
|
79
|
-
/** Build the default "aborted" fallback result. */
|
|
80
|
-
function abortedResult(bestResult: SessionResult | null): SessionResult {
|
|
81
|
-
return bestResult ?? {
|
|
82
|
-
completed: false, needsHandoff: false, totalTokens: 0, sessionId: '',
|
|
83
|
-
error: 'Execution stopped by user',
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Create a HeadlessRunner configured for the current retry iteration. */
|
|
88
|
-
function createRunner(
|
|
89
|
-
config: IssueRunnerConfig,
|
|
90
|
-
state: IssueRetryState,
|
|
91
|
-
useResume: boolean,
|
|
92
|
-
resumeSessionId: string | undefined,
|
|
93
|
-
): HeadlessRunner {
|
|
94
|
-
return new HeadlessRunner({
|
|
19
|
+
export async function runIssueWithRetry(config: IssueRunnerConfig): Promise<SessionResult> {
|
|
20
|
+
const runner = new ResilientRunner({
|
|
95
21
|
workingDir: config.workingDir,
|
|
96
|
-
|
|
22
|
+
prompt: config.prompt,
|
|
23
|
+
policy: 'FULL',
|
|
24
|
+
maxRetries: 3,
|
|
97
25
|
stallWarningMs: config.stallWarningMs,
|
|
98
26
|
stallKillMs: config.stallKillMs,
|
|
99
27
|
stallHardCapMs: config.stallHardCapMs,
|
|
100
28
|
stallMaxExtensions: config.stallMaxExtensions,
|
|
101
|
-
verbose: true,
|
|
102
|
-
continueSession: useResume,
|
|
103
|
-
claudeSessionId: resumeSessionId,
|
|
104
29
|
outputCallback: config.outputCallback,
|
|
105
|
-
|
|
106
|
-
state.checkpoint = cp;
|
|
107
|
-
},
|
|
30
|
+
verbose: true,
|
|
108
31
|
extraEnv: config.extraEnv,
|
|
32
|
+
abortSignal: config.abortSignal,
|
|
33
|
+
onRetry: (info) => {
|
|
34
|
+
config.outputCallback?.(
|
|
35
|
+
`\n[PM-RETRY] Auto-retry ${info.retryNumber}/${info.maxRetries}: ${info.path} — ${info.reason}\n`,
|
|
36
|
+
);
|
|
37
|
+
},
|
|
109
38
|
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Wire the abort signal to clean up the runner. Returns a cleanup function. */
|
|
113
|
-
function wireAbortSignal(
|
|
114
|
-
runner: HeadlessRunner,
|
|
115
|
-
abortSignal: AbortSignal | undefined,
|
|
116
|
-
): (() => void) | null {
|
|
117
|
-
if (!abortSignal) return null;
|
|
118
|
-
|
|
119
|
-
const abortHandler = () => { runner.cleanup(); };
|
|
120
|
-
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
121
|
-
return () => abortSignal.removeEventListener('abort', abortHandler);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Run a single iteration: spawn runner, await result, evaluate retry.
|
|
126
|
-
* Returns { result, shouldRetry } — caller loops while shouldRetry is true.
|
|
127
|
-
* Returns null if aborted (caller should return abortedResult).
|
|
128
|
-
*/
|
|
129
|
-
async function runSingleAttempt(
|
|
130
|
-
config: IssueRunnerConfig,
|
|
131
|
-
state: IssueRetryState,
|
|
132
|
-
): Promise<{ result: SessionResult; shouldRetry: boolean } | null> {
|
|
133
|
-
state.checkpoint = null;
|
|
134
|
-
|
|
135
|
-
const useResume = !!state.lastSessionId;
|
|
136
|
-
const resumeSessionId = state.lastSessionId;
|
|
137
|
-
state.lastSessionId = undefined;
|
|
138
|
-
|
|
139
|
-
const runner = createRunner(config, state, useResume, resumeSessionId);
|
|
140
|
-
|
|
141
|
-
if (config.abortSignal?.aborted) {
|
|
142
|
-
runner.cleanup();
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
const removeAbortListener = wireAbortSignal(runner, config.abortSignal);
|
|
146
|
-
|
|
147
|
-
const result = await runner.run();
|
|
148
|
-
removeAbortListener?.();
|
|
149
|
-
|
|
150
|
-
if (config.abortSignal?.aborted) return null;
|
|
151
|
-
|
|
152
|
-
// Track best result for fallback selection
|
|
153
|
-
if (!state.bestResult || scoreResult(result) > scoreResult(state.bestResult)) {
|
|
154
|
-
state.bestResult = result;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Evaluate retry strategies in priority order
|
|
158
|
-
const shouldRetry =
|
|
159
|
-
tryToolTimeoutRetry(state, result, config) ||
|
|
160
|
-
trySignalCrashRetry(state, result, config) ||
|
|
161
|
-
await tryPrematureCompletionRetry(state, result, config);
|
|
162
|
-
|
|
163
|
-
return { result, shouldRetry };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function runIssueWithRetry(config: IssueRunnerConfig): Promise<SessionResult> {
|
|
167
|
-
const state: IssueRetryState = {
|
|
168
|
-
currentPrompt: config.prompt,
|
|
169
|
-
retryNumber: 0,
|
|
170
|
-
checkpoint: null,
|
|
171
|
-
accumulatedToolResults: [],
|
|
172
|
-
timedOutTools: [],
|
|
173
|
-
lastSessionId: undefined,
|
|
174
|
-
bestResult: null,
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
let result: SessionResult | undefined;
|
|
178
|
-
|
|
179
|
-
while (state.retryNumber <= MAX_ISSUE_RETRIES) {
|
|
180
|
-
if (config.abortSignal?.aborted) return abortedResult(state.bestResult);
|
|
181
|
-
|
|
182
|
-
const attempt = await runSingleAttempt(config, state);
|
|
183
|
-
if (!attempt) {
|
|
184
|
-
return state.bestResult ?? result ?? abortedResult(null);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
result = attempt.result;
|
|
188
|
-
if (!attempt.shouldRetry) break;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return result ?? state.bestResult ?? {
|
|
192
|
-
completed: false,
|
|
193
|
-
needsHandoff: false,
|
|
194
|
-
totalTokens: 0,
|
|
195
|
-
sessionId: '',
|
|
196
|
-
error: 'No result produced after retries',
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ========== Retry Strategies ==========
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Strategy 1: Tool timeout checkpoint recovery.
|
|
204
|
-
* When a tool times out, we have a checkpoint with all completed tools.
|
|
205
|
-
* Build a new prompt injecting those results and skip the hung resource.
|
|
206
|
-
*/
|
|
207
|
-
function tryToolTimeoutRetry(
|
|
208
|
-
state: IssueRetryState,
|
|
209
|
-
_result: SessionResult,
|
|
210
|
-
config: IssueRunnerConfig,
|
|
211
|
-
): boolean {
|
|
212
|
-
if (!state.checkpoint || state.retryNumber >= MAX_ISSUE_RETRIES) return false;
|
|
213
|
-
|
|
214
|
-
const cp = state.checkpoint;
|
|
215
|
-
state.retryNumber++;
|
|
216
|
-
|
|
217
|
-
state.timedOutTools.push({
|
|
218
|
-
toolName: cp.hungTool.toolName,
|
|
219
|
-
input: cp.hungTool.input ?? {},
|
|
220
|
-
timeoutMs: cp.hungTool.timeoutMs,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const canResume = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
|
|
224
|
-
|
|
225
|
-
hlog(`[PM-RETRY] Tool timeout: ${cp.hungTool.toolName} after ${Math.round(cp.hungTool.timeoutMs / 1000)}s, ${cp.completedTools.length} tools completed, retry ${state.retryNumber}/${MAX_ISSUE_RETRIES} (${canResume ? 'resume' : 'fresh'})`);
|
|
226
|
-
|
|
227
|
-
if (canResume) {
|
|
228
|
-
state.lastSessionId = cp.claudeSessionId;
|
|
229
|
-
state.currentPrompt = buildResumeRetryPrompt(cp, state.timedOutTools);
|
|
230
|
-
} else {
|
|
231
|
-
state.currentPrompt = buildRetryPrompt(cp, config.prompt, state.timedOutTools);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
config.outputCallback?.(`\n[PM-RETRY] Auto-retry ${state.retryNumber}/${MAX_ISSUE_RETRIES}: ${canResume ? 'Resuming session' : 'Continuing'} with ${cp.completedTools.length} results, skipping failed ${cp.hungTool.toolName}.\n`);
|
|
235
|
-
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Strategy 2: Signal crash recovery.
|
|
241
|
-
* Process was killed by signal (SIGTERM/SIGKILL from stall watchdog or OS).
|
|
242
|
-
* Accumulate completed tools and retry with preserved context.
|
|
243
|
-
*/
|
|
244
|
-
function trySignalCrashRetry(
|
|
245
|
-
state: IssueRetryState,
|
|
246
|
-
result: SessionResult,
|
|
247
|
-
config: IssueRunnerConfig,
|
|
248
|
-
): boolean {
|
|
249
|
-
const isSignalCrash = !!result.signalName;
|
|
250
|
-
const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
|
|
251
|
-
if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= MAX_ISSUE_RETRIES) return false;
|
|
252
|
-
// Don't double-handle if a checkpoint was already captured (tool timeout takes priority)
|
|
253
|
-
if (state.checkpoint) return false;
|
|
254
|
-
|
|
255
|
-
accumulateToolResults(result, state);
|
|
256
|
-
state.retryNumber++;
|
|
257
|
-
|
|
258
|
-
const signalInfo = result.signalName || 'unknown signal';
|
|
259
|
-
const useResume = !!result.claudeSessionId && state.retryNumber === 1;
|
|
260
|
-
|
|
261
|
-
hlog(`[PM-RETRY] Signal crash: ${signalInfo}, ${state.accumulatedToolResults.length} tools preserved, retry ${state.retryNumber}/${MAX_ISSUE_RETRIES} (${useResume ? 'resume' : 'fresh'})`);
|
|
262
|
-
|
|
263
|
-
if (useResume) {
|
|
264
|
-
state.lastSessionId = result.claudeSessionId;
|
|
265
|
-
state.currentPrompt = buildSignalCrashRecoveryPrompt(config.prompt, true);
|
|
266
|
-
} else {
|
|
267
|
-
state.currentPrompt = buildSignalCrashRecoveryPrompt(
|
|
268
|
-
config.prompt,
|
|
269
|
-
false,
|
|
270
|
-
state.accumulatedToolResults,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
config.outputCallback?.(`\n[PM-RETRY] Signal recovery ${state.retryNumber}/${MAX_ISSUE_RETRIES}: ${useResume ? 'Resuming' : 'Restarting'} with ${state.accumulatedToolResults.length} preserved results.\n`);
|
|
275
|
-
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Check if an end_turn result is actually incomplete using Haiku assessment. */
|
|
280
|
-
async function isEndTurnIncomplete(result: SessionResult): Promise<boolean> {
|
|
281
|
-
if (!result.assistantResponse) return false;
|
|
282
|
-
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
283
|
-
try {
|
|
284
|
-
const verdict = await assessPrematureCompletion({
|
|
285
|
-
responseTail: result.assistantResponse.slice(-800),
|
|
286
|
-
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
287
|
-
hasThinking: !!result.thinkingOutput,
|
|
288
|
-
responseLength: result.assistantResponse.length,
|
|
289
|
-
}, claudeCmd, true);
|
|
290
|
-
|
|
291
|
-
hlog(`[PM-RETRY] Premature completion check: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
|
|
292
|
-
return verdict.isIncomplete;
|
|
293
|
-
} catch {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Strategy 3: Premature completion.
|
|
300
|
-
* Claude hit max_tokens or ended early without finishing work.
|
|
301
|
-
* Resume the session with "continue".
|
|
302
|
-
*/
|
|
303
|
-
async function tryPrematureCompletionRetry(
|
|
304
|
-
state: IssueRetryState,
|
|
305
|
-
result: SessionResult,
|
|
306
|
-
config: IssueRunnerConfig,
|
|
307
|
-
): Promise<boolean> {
|
|
308
|
-
if (!result.completed || result.signalName || state.retryNumber >= MAX_ISSUE_RETRIES) return false;
|
|
309
|
-
if (state.checkpoint) return false;
|
|
310
|
-
if (!result.claudeSessionId || !result.stopReason) return false;
|
|
311
|
-
|
|
312
|
-
const isMaxTokens = result.stopReason === 'max_tokens';
|
|
313
|
-
const isEndTurn = result.stopReason === 'end_turn';
|
|
314
|
-
if (!isMaxTokens && !isEndTurn) return false;
|
|
315
|
-
|
|
316
|
-
// max_tokens always continues; end_turn requires AI assessment
|
|
317
|
-
if (isEndTurn && !(await isEndTurnIncomplete(result))) return false;
|
|
318
|
-
|
|
319
|
-
state.retryNumber++;
|
|
320
|
-
state.lastSessionId = result.claudeSessionId;
|
|
321
|
-
state.currentPrompt = 'continue';
|
|
322
|
-
|
|
323
|
-
const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished';
|
|
324
|
-
hlog(`[PM-RETRY] Premature completion: ${reason}, resuming session, retry ${state.retryNumber}/${MAX_ISSUE_RETRIES}`);
|
|
325
|
-
config.outputCallback?.(`\n[PM-RETRY] ${reason} — resuming session (retry ${state.retryNumber}/${MAX_ISSUE_RETRIES}).\n`);
|
|
326
|
-
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ========== Helpers ==========
|
|
331
|
-
|
|
332
|
-
function accumulateToolResults(result: SessionResult, state: IssueRetryState): void {
|
|
333
|
-
if (!result.toolUseHistory) return;
|
|
334
|
-
for (const t of result.toolUseHistory) {
|
|
335
|
-
if (t.result !== undefined) {
|
|
336
|
-
state.accumulatedToolResults.push({
|
|
337
|
-
toolName: t.toolName,
|
|
338
|
-
toolId: t.toolId,
|
|
339
|
-
toolInput: t.toolInput,
|
|
340
|
-
result: t.result,
|
|
341
|
-
isError: t.isError,
|
|
342
|
-
duration: t.duration,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
if (state.accumulatedToolResults.length > MAX_ACCUMULATED_RESULTS) {
|
|
347
|
-
state.accumulatedToolResults = state.accumulatedToolResults.slice(-MAX_ACCUMULATED_RESULTS);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
39
|
|
|
351
|
-
|
|
352
|
-
const toolCount = r.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
353
|
-
const responseLen = Math.min((r.assistantResponse?.length ?? 0) / 50, 100);
|
|
354
|
-
const hasThinking = r.thinkingOutput ? 20 : 0;
|
|
355
|
-
return toolCount * 10 + responseLen + hasThinking;
|
|
40
|
+
return runner.run();
|
|
356
41
|
}
|
|
@@ -286,6 +286,7 @@ export function parseIssue(content: string, filePath: string): Issue {
|
|
|
286
286
|
filesToModify: parseListItems(sections.get('Files to Modify') || ''),
|
|
287
287
|
activity: parseListItems(sections.get('Activity') || ''),
|
|
288
288
|
reviewGate: (['none', 'auto', 'required'].includes(String(fm.review_gate)) ? String(fm.review_gate) : 'auto') as Issue['reviewGate'],
|
|
289
|
+
outputType: (['code', 'document', 'auto'].includes(String(fm.output_type)) ? String(fm.output_type) : 'auto') as Issue['outputType'],
|
|
289
290
|
outputFile: optionalString(fm.output_file),
|
|
290
291
|
body,
|
|
291
292
|
path: filePath,
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import {
|
|
15
|
-
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
14
|
+
import { ResilientRunner } from '../../cli/headless/resilient-runner.js';
|
|
16
15
|
import { loadAgentPrompt } from './agent-loader.js';
|
|
16
|
+
import { resolveIsCodeTask } from './issue-classification.js';
|
|
17
17
|
import type { Issue, ReviewCheck, ReviewResult } from './types.js';
|
|
18
18
|
|
|
19
19
|
/** Max review attempts per issue per sprint before giving up */
|
|
@@ -46,24 +46,27 @@ export interface ReviewIssueOptions {
|
|
|
46
46
|
*/
|
|
47
47
|
export async function reviewIssue(options: ReviewIssueOptions): Promise<ReviewResult> {
|
|
48
48
|
const { workingDir, issue, pmDir, outputPath, onOutput, logDir, reviewCriteria, boardDir } = options;
|
|
49
|
-
const isCodeTask = issue
|
|
49
|
+
const isCodeTask = resolveIsCodeTask(issue);
|
|
50
50
|
const issueType: ReviewResult['issueType'] = isCodeTask ? 'code' : 'non-code';
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
53
|
const prompt = buildReviewPrompt(issue, pmDir, outputPath, isCodeTask, reviewCriteria, boardDir, workingDir);
|
|
54
54
|
|
|
55
|
-
const runner = new
|
|
55
|
+
const runner = new ResilientRunner({
|
|
56
56
|
workingDir,
|
|
57
|
-
|
|
57
|
+
prompt,
|
|
58
|
+
policy: 'STANDARD',
|
|
58
59
|
stallWarningMs: REVIEW_STALL_WARNING_MS,
|
|
59
60
|
stallKillMs: REVIEW_STALL_KILL_MS,
|
|
60
61
|
stallHardCapMs: REVIEW_STALL_HARD_CAP_MS,
|
|
61
62
|
verbose: true,
|
|
62
63
|
outputCallback: onOutput ? (text: string) => onOutput(`Review: ${text}`) : undefined,
|
|
63
64
|
extraEnv: options.extraEnv,
|
|
65
|
+
logLabel: 'pm-review',
|
|
66
|
+
logDir,
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
const result = await
|
|
69
|
+
const result = await runner.run();
|
|
67
70
|
|
|
68
71
|
if (result.completed && result.assistantResponse) {
|
|
69
72
|
return parseReviewOutput(issue.id, issueType, result.assistantResponse);
|
|
@@ -94,6 +94,9 @@ export interface Issue {
|
|
|
94
94
|
progress: string | null;
|
|
95
95
|
// Review gate mode (none = skip review, auto = AI review, required = human review)
|
|
96
96
|
reviewGate: 'none' | 'auto' | 'required';
|
|
97
|
+
// What the issue produces — drives prompt construction and review strategy
|
|
98
|
+
// code = must modify source files, document = produce written artifact, auto = infer from context
|
|
99
|
+
outputType: 'code' | 'document' | 'auto';
|
|
97
100
|
// Planned output file path (from front matter output_file, relative to working dir)
|
|
98
101
|
outputFile: string | null;
|
|
99
102
|
// Full markdown body
|
|
@@ -24,7 +24,13 @@ export const CLI_VERSION = (() => {
|
|
|
24
24
|
})()
|
|
25
25
|
|
|
26
26
|
const MSTRO_DIR = join(homedir(), '.mstro')
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
function credentialsFile(): string {
|
|
29
|
+
const env = process.env.MSTRO_ENV || 'production'
|
|
30
|
+
if (env === 'staging') return join(MSTRO_DIR, 'credentials-staging.json')
|
|
31
|
+
if (env === 'dev') return join(MSTRO_DIR, 'credentials-dev.json')
|
|
32
|
+
return join(MSTRO_DIR, 'credentials.json')
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
/** Refresh token every 30 days */
|
|
30
36
|
export const TOKEN_REFRESH_INTERVAL_MS = 30 * 24 * 60 * 60 * 1000
|
|
@@ -42,11 +48,11 @@ export interface StoredCredentials {
|
|
|
42
48
|
* Get stored credentials from ~/.mstro/credentials.json
|
|
43
49
|
*/
|
|
44
50
|
export function getCredentials(): StoredCredentials | null {
|
|
45
|
-
if (!existsSync(
|
|
51
|
+
if (!existsSync(credentialsFile())) {
|
|
46
52
|
return null
|
|
47
53
|
}
|
|
48
54
|
try {
|
|
49
|
-
const content = readFileSync(
|
|
55
|
+
const content = readFileSync(credentialsFile(), 'utf-8')
|
|
50
56
|
const creds = JSON.parse(content)
|
|
51
57
|
if (creds.token && creds.userId && creds.email) {
|
|
52
58
|
return creds
|
|
@@ -64,7 +70,7 @@ export function updateCredentials(updates: Partial<StoredCredentials>): void {
|
|
|
64
70
|
const creds = getCredentials()
|
|
65
71
|
if (!creds) return
|
|
66
72
|
|
|
67
|
-
writeFileSync(
|
|
73
|
+
writeFileSync(credentialsFile(), JSON.stringify({ ...creds, ...updates }, null, 2), {
|
|
68
74
|
mode: 0o600
|
|
69
75
|
})
|
|
70
76
|
}
|
|
@@ -72,7 +72,13 @@ export class PTYManager extends EventEmitter {
|
|
|
72
72
|
return { shell: existingSession.shell, cwd: existingSession.cwd, isReconnect: true, platform: platform() };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const ALLOWED_SHELLS = new Set([
|
|
76
|
+
'/bin/sh', '/bin/bash', '/bin/zsh', '/usr/bin/bash', '/usr/bin/zsh',
|
|
77
|
+
'/usr/bin/fish', '/usr/local/bin/bash', '/usr/local/bin/zsh', '/usr/local/bin/fish',
|
|
78
|
+
'/bin/dash', '/usr/bin/dash',
|
|
79
|
+
'powershell.exe', 'pwsh.exe', 'cmd.exe',
|
|
80
|
+
]);
|
|
81
|
+
const shell = (requestedShell && ALLOWED_SHELLS.has(requestedShell)) ? requestedShell : detectShell();
|
|
76
82
|
const cwd = workingDir || homedir();
|
|
77
83
|
|
|
78
84
|
try {
|
|
@@ -7,6 +7,7 @@ import type { AutocompleteService } from './autocomplete.js';
|
|
|
7
7
|
import type { FileUploadHandler } from './file-upload-handler.js';
|
|
8
8
|
import type { GitHeadWatcher } from './git-head-watcher.js';
|
|
9
9
|
import type { SessionRegistry } from './session-registry.js';
|
|
10
|
+
import type { SkillsWatcher } from './skill-watcher.js';
|
|
10
11
|
import type { WebSocketResponse, WSContext } from './types.js';
|
|
11
12
|
|
|
12
13
|
export interface UsageReport {
|
|
@@ -35,6 +36,7 @@ export interface HandlerContext {
|
|
|
35
36
|
usageReporter: UsageReporter | null;
|
|
36
37
|
fileUploadHandler: FileUploadHandler | null;
|
|
37
38
|
gitHeadWatcher: GitHeadWatcher | null;
|
|
39
|
+
skillsWatcher: SkillsWatcher | null;
|
|
38
40
|
|
|
39
41
|
// Registry access
|
|
40
42
|
getRegistry(workingDir: string): SessionRegistry;
|
|
@@ -15,7 +15,6 @@ import { dirname, join } from 'node:path';
|
|
|
15
15
|
import type { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
16
16
|
import { captureException } from '../sentry.js';
|
|
17
17
|
import { AutocompleteService } from './autocomplete.js';
|
|
18
|
-
import { handleDeployMessage } from './deploy-handlers.js';
|
|
19
18
|
import { handleFileExplorerMessage, handleFileMessage } from './file-explorer-handlers.js';
|
|
20
19
|
import { FileUploadHandler } from './file-upload-handler.js';
|
|
21
20
|
import { handleGitMessage } from './git-handlers.js';
|
|
@@ -27,6 +26,7 @@ import { handleHistoryMessage, handleSessionMessage, initializeTab, resumeHistor
|
|
|
27
26
|
import { SessionRegistry } from './session-registry.js';
|
|
28
27
|
import { generateNotificationSummary, handleGetSettings, handleUpdateSettings } from './settings-handlers.js';
|
|
29
28
|
import { handleListSkills } from './skill-handlers.js';
|
|
29
|
+
import { SkillsWatcher } from './skill-watcher.js';
|
|
30
30
|
import { handleCreateTab, handleGetActiveTabs, handleMarkTabViewed, handleRemoveTab, handleReorderTabs, handleSyncPromptText, handleSyncTabMeta } from './tab-handlers.js';
|
|
31
31
|
import { cleanupTerminalSubscribers, handleTerminalMessage } from './terminal-handlers.js';
|
|
32
32
|
import type { FrecencyData, WebSocketMessage, WebSocketResponse, WSContext } from './types.js';
|
|
@@ -48,17 +48,21 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
48
48
|
terminalSubscribers: Map<string, Set<WSContext>> = new Map();
|
|
49
49
|
fileUploadHandler: FileUploadHandler | null = null;
|
|
50
50
|
gitHeadWatcher: GitHeadWatcher | null = null;
|
|
51
|
+
skillsWatcher: SkillsWatcher | null = null;
|
|
51
52
|
|
|
52
53
|
constructor() {
|
|
53
54
|
this.frecencyPath = join(homedir(), '.mstro', 'autocomplete-frecency.json');
|
|
54
55
|
const frecencyData = this.loadFrecencyData();
|
|
55
56
|
this.autocompleteService = new AutocompleteService(frecencyData);
|
|
57
|
+
process.on('exit', () => {
|
|
58
|
+
if (this.frecencySaveTimer) {
|
|
59
|
+
clearTimeout(this.frecencySaveTimer);
|
|
60
|
+
this.saveFrecencyData();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
getRegistry(workingDir: string): SessionRegistry {
|
|
59
|
-
if (!this.sessionRegistry && workingDir) {
|
|
60
|
-
this.sessionRegistry = new SessionRegistry(workingDir);
|
|
61
|
-
}
|
|
62
66
|
if (!this.sessionRegistry) {
|
|
63
67
|
this.sessionRegistry = new SessionRegistry(workingDir);
|
|
64
68
|
}
|
|
@@ -113,6 +117,10 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
113
117
|
this.gitHeadWatcher = new GitHeadWatcher(workingDir, this);
|
|
114
118
|
this.gitHeadWatcher.start();
|
|
115
119
|
}
|
|
120
|
+
if (!this.skillsWatcher && workingDir) {
|
|
121
|
+
this.skillsWatcher = new SkillsWatcher(workingDir, this);
|
|
122
|
+
this.skillsWatcher.start();
|
|
123
|
+
}
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
async handleMessage(
|
|
@@ -138,7 +146,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
/** Dispatch table mapping message types to domain handlers. Built once, looked up per message. */
|
|
141
|
-
private static readonly DISPATCH: Record<string, 'session' | 'history' | 'file' | 'terminal' | 'fileExplorer' | 'git' | 'quality' | 'plan' | 'fileUpload'
|
|
149
|
+
private static readonly DISPATCH: Record<string, 'session' | 'history' | 'file' | 'terminal' | 'fileExplorer' | 'git' | 'quality' | 'plan' | 'fileUpload'> = {
|
|
142
150
|
// Session
|
|
143
151
|
execute: 'session', cancel: 'session', getHistory: 'session', new: 'session', approve: 'session', reject: 'session',
|
|
144
152
|
// History
|
|
@@ -157,8 +165,6 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
157
165
|
planInit: 'plan', planGetState: 'plan', planListIssues: 'plan', planGetIssue: 'plan', planGetSprint: 'plan', planGetMilestone: 'plan', planCreateIssue: 'plan', planUpdateIssue: 'plan', planDeleteIssue: 'plan', planScaffold: 'plan', planPrompt: 'plan', planExecute: 'plan', planExecuteEpic: 'plan', planPause: 'plan', planStop: 'plan', planResume: 'plan', planCreateBoard: 'plan', planUpdateBoard: 'plan', planArchiveBoard: 'plan', planGetBoard: 'plan', planGetBoardState: 'plan', planReorderBoards: 'plan', planSetActiveBoard: 'plan', planGetBoardArtifacts: 'plan', planCreateSprint: 'plan', planActivateSprint: 'plan', planCompleteSprint: 'plan', planGetSprintArtifacts: 'plan', chatToBoard: 'plan',
|
|
158
166
|
// File upload
|
|
159
167
|
fileUploadStart: 'fileUpload', fileUploadChunk: 'fileUpload', fileUploadComplete: 'fileUpload', fileUploadCancel: 'fileUpload',
|
|
160
|
-
// Deploy management + HTTP relay
|
|
161
|
-
deployCreate: 'deploy', deployStop: 'deploy', deployResume: 'deploy', deployDelete: 'deploy', deployList: 'deploy', deployGetStatus: 'deploy', deployUpdateConfig: 'deploy', deploySetApiKey: 'deploy', deployValidateApiKey: 'deploy', deployHttpRequest: 'deploy',
|
|
162
168
|
};
|
|
163
169
|
|
|
164
170
|
private async dispatchMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): Promise<void> {
|
|
@@ -192,6 +198,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
192
198
|
case 'getSettings':
|
|
193
199
|
return handleGetSettings(this, ws);
|
|
194
200
|
case 'updateSettings':
|
|
201
|
+
if (permission === 'view') return;
|
|
195
202
|
return handleUpdateSettings(this, ws, msg);
|
|
196
203
|
case 'listSkills':
|
|
197
204
|
return handleListSkills(this, ws, workingDir);
|
|
@@ -214,7 +221,6 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
214
221
|
case 'quality': return handleQualityMessage(this, ws, msg, tabId, workingDir, permission);
|
|
215
222
|
case 'plan': return handlePlanMessage(this, ws, msg, tabId, workingDir, permission);
|
|
216
223
|
case 'fileUpload': return this.handleFileUploadMessage(ws, msg, tabId, workingDir);
|
|
217
|
-
case 'deploy': return handleDeployMessage(this, ws, msg, tabId, workingDir, permission);
|
|
218
224
|
}
|
|
219
225
|
}
|
|
220
226
|
|
|
@@ -260,6 +266,10 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
260
266
|
this.gitHeadWatcher.stop();
|
|
261
267
|
this.gitHeadWatcher = null;
|
|
262
268
|
}
|
|
269
|
+
if (this.skillsWatcher) {
|
|
270
|
+
this.skillsWatcher.stop();
|
|
271
|
+
this.skillsWatcher = null;
|
|
272
|
+
}
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
275
|
|