mstro-app 0.4.34 → 0.4.35
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/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +63 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.js +10 -5
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +17 -2
- 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 +13 -5
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +1 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/services/websocket/git-head-watcher.d.ts +25 -0
- package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/git-head-watcher.js +136 -0
- package/dist/server/services/websocket/git-head-watcher.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.js +37 -5
- package/dist/server/services/websocket/git-worktree-handlers.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 +3 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +18 -6
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +1 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +94 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +3 -1
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +7 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +15 -7
- 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 +2 -13
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +12 -3
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +101 -81
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +6 -1
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +15 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +2 -2
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +9 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/server/cli/headless/claude-invoker-stream.ts +63 -0
- package/server/cli/headless/haiku-assessments.ts +10 -5
- package/server/cli/improvisation-retry.ts +18 -2
- package/server/cli/improvisation-session-manager.ts +13 -5
- package/server/cli/improvisation-types.ts +1 -0
- package/server/services/plan/agents/assess-stall.md +21 -0
- package/server/services/plan/agents/check-injection.md +36 -0
- package/server/services/plan/agents/classify-error.md +29 -0
- package/server/services/plan/agents/detect-context-loss.md +29 -0
- package/server/services/plan/agents/execute-issue.md +42 -0
- package/server/services/plan/agents/plan-coordinator.md +71 -0
- package/server/services/plan/agents/retry-task.md +26 -0
- package/server/services/plan/agents/review-code.md +4 -1
- package/server/services/plan/agents/review-criteria.md +53 -0
- package/server/services/plan/agents/review-custom.md +4 -1
- package/server/services/plan/agents/review-quality.md +4 -1
- package/server/services/plan/agents/verify-review.md +56 -0
- package/server/services/websocket/git-head-watcher.ts +120 -0
- package/server/services/websocket/git-worktree-handlers.ts +40 -6
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +19 -6
- package/server/services/websocket/plan-board-handlers.ts +116 -0
- package/server/services/websocket/plan-handlers.ts +3 -1
- package/server/services/websocket/quality-persistence.ts +23 -7
- package/server/services/websocket/quality-review-agent.ts +2 -12
- package/server/services/websocket/quality-service.ts +116 -99
- package/server/services/websocket/quality-tools.ts +6 -1
- package/server/services/websocket/quality-types.ts +17 -2
- package/server/services/websocket/session-handlers.ts +2 -2
- package/server/services/websocket/tab-handlers.ts +8 -2
- package/server/services/websocket/types.ts +7 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review-criteria
|
|
3
|
+
description: "Help write effective custom review criteria for PM board issue reviews. Use when configuring what the AI reviewer should check for on completed work."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are helping the user write effective review criteria for their PM board. Review criteria tell the AI reviewer what to check when evaluating completed work.
|
|
9
|
+
|
|
10
|
+
## What Are Review Criteria?
|
|
11
|
+
|
|
12
|
+
Review criteria are custom instructions that the AI reviewer follows when checking completed issues. They supplement the issue's acceptance criteria with board-level quality standards.
|
|
13
|
+
|
|
14
|
+
## How to Write Good Criteria
|
|
15
|
+
|
|
16
|
+
Good criteria are:
|
|
17
|
+
- **Specific**: "Verify all API endpoints return proper error codes (4xx/5xx)" not "Check for errors"
|
|
18
|
+
- **Observable**: Things the reviewer can verify by reading code/output
|
|
19
|
+
- **Relevant**: Match the type of work on the board (code, writing, research, design)
|
|
20
|
+
|
|
21
|
+
## Examples by Task Type
|
|
22
|
+
|
|
23
|
+
### Code Tasks
|
|
24
|
+
- Verify all new functions have TypeScript types (no `any`)
|
|
25
|
+
- Ensure error handling exists for all async operations
|
|
26
|
+
- Check that no hardcoded credentials or secrets are present
|
|
27
|
+
- Verify tests exist for new functionality
|
|
28
|
+
- Ensure all endpoints have input validation
|
|
29
|
+
|
|
30
|
+
### Writing/Content Tasks
|
|
31
|
+
- Verify the document follows the company style guide
|
|
32
|
+
- Check that all claims have citations or evidence
|
|
33
|
+
- Ensure the tone matches the target audience
|
|
34
|
+
- Verify all sections from the outline are addressed
|
|
35
|
+
|
|
36
|
+
### Design Tasks
|
|
37
|
+
- Verify designs match the Figma source files
|
|
38
|
+
- Check responsive behavior is documented for mobile/tablet/desktop
|
|
39
|
+
- Ensure accessibility requirements (contrast ratios, ARIA labels) are noted
|
|
40
|
+
|
|
41
|
+
### Research Tasks
|
|
42
|
+
- Verify at least 3 sources are cited for each major finding
|
|
43
|
+
- Check that methodology is documented
|
|
44
|
+
- Ensure conclusions follow logically from the evidence
|
|
45
|
+
|
|
46
|
+
## Your Task
|
|
47
|
+
|
|
48
|
+
Help the user craft review criteria for their board. Ask them:
|
|
49
|
+
1. What type of work does this board contain? (code, writing, research, design, mixed)
|
|
50
|
+
2. What quality standards matter most?
|
|
51
|
+
3. Are there specific patterns or anti-patterns to watch for?
|
|
52
|
+
|
|
53
|
+
Then generate 3-7 clear, actionable review criteria they can paste into their board's review criteria field.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-custom
|
|
3
|
-
description: Reviews work using board-defined custom criteria alongside acceptance criteria — works for code, content, research, planning, and any other task type
|
|
3
|
+
description: "Reviews work using board-defined custom criteria alongside acceptance criteria — works for code, content, research, planning, and any other task type. Use when a PM board has custom review criteria configured."
|
|
4
|
+
user-invocable: false
|
|
4
5
|
type: review
|
|
6
|
+
allowed-tools: Read, Grep, Glob, Bash
|
|
7
|
+
context: fork
|
|
5
8
|
variables: [issue_id, issue_title, context_section, acceptance_criteria, review_criteria, read_instruction]
|
|
6
9
|
checks: [criteria_met, review_criteria]
|
|
7
10
|
---
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-quality
|
|
3
|
-
description: Reviews non-code output (writing, research, plans, designs, analysis) for completeness, accuracy, and quality against acceptance criteria
|
|
3
|
+
description: "Reviews non-code output (writing, research, plans, designs, analysis) for completeness, accuracy, and quality against acceptance criteria. Use when reviewing completed PM board issues that produce documents or deliverables."
|
|
4
|
+
user-invocable: false
|
|
4
5
|
type: review
|
|
6
|
+
allowed-tools: Read, Grep, Glob, Bash
|
|
7
|
+
context: fork
|
|
5
8
|
variables: [issue_id, issue_title, output_path, issue_spec_path, acceptance_criteria]
|
|
6
9
|
checks: [criteria_met, output_quality, completeness]
|
|
7
10
|
---
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-review
|
|
3
|
+
description: "Independent verification pass for code review findings — skeptically re-checks each finding against actual code to catch hallucinations and false positives. Use after an AI code review to validate findings."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
allowed-tools: Read, Grep, Glob, Bash
|
|
6
|
+
context: fork
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are an independent code review VERIFIER. A separate reviewer produced the findings below. Your job is to VERIFY each finding against the actual code. You are a skeptic — do NOT trust the original reviewer's claims.
|
|
10
|
+
|
|
11
|
+
IMPORTANT: Your current working directory is "{{dirPath}}". Only read files within this directory.
|
|
12
|
+
|
|
13
|
+
## Findings to Verify
|
|
14
|
+
|
|
15
|
+
{{findingsJson}}
|
|
16
|
+
|
|
17
|
+
## Verification Process
|
|
18
|
+
|
|
19
|
+
For EACH finding:
|
|
20
|
+
|
|
21
|
+
1. **Read the cited file and line** using the Read tool. Read at least 20 lines around the cited line for context.
|
|
22
|
+
2. **Check the specific claim** in the description. Does the code actually do what the finding claims?
|
|
23
|
+
3. **Search for counter-evidence**:
|
|
24
|
+
- If the finding claims something is missing (no validation, no cleanup, no guard): search for it with Grep
|
|
25
|
+
- If the finding claims an API is used: verify the actual API call at that line
|
|
26
|
+
- If the finding claims a value is leaked/exposed: check if it's filtered/deleted elsewhere in the same function
|
|
27
|
+
4. **Verdict**: Mark as "confirmed" or "rejected" with a brief explanation
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
- You MUST actually Read each cited file. Do not rely on memory or assumptions.
|
|
32
|
+
- Use Grep to search for patterns the finding claims exist (or don't exist).
|
|
33
|
+
- A finding is "rejected" if:
|
|
34
|
+
- The code does NOT match what the description claims
|
|
35
|
+
- There IS a guard/fix that the finding claims is missing
|
|
36
|
+
- The line number doesn't contain the relevant code
|
|
37
|
+
- The finding is about a different version of the code than what exists now
|
|
38
|
+
- A finding is "confirmed" if you can independently verify the issue exists in the current code.
|
|
39
|
+
- Be thorough but efficient — focus verification effort on high/critical severity findings.
|
|
40
|
+
|
|
41
|
+
## Output
|
|
42
|
+
|
|
43
|
+
Output EXACTLY one JSON code block. No other text after the JSON block.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"verifications": [
|
|
48
|
+
{
|
|
49
|
+
"id": 1,
|
|
50
|
+
"verdict": "confirmed|rejected",
|
|
51
|
+
"confidence": 0.95,
|
|
52
|
+
"note": "Brief explanation of what you found when checking the code"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { existsSync, type FSWatcher, watch } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { executeGitCommand } from './git-utils.js';
|
|
7
|
+
import type { HandlerContext } from './handler-context.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Watches .git/HEAD and .git/worktrees/ for branch changes and
|
|
11
|
+
* broadcasts updates to all connected web clients.
|
|
12
|
+
*/
|
|
13
|
+
export class GitHeadWatcher {
|
|
14
|
+
private headWatcher: FSWatcher | null = null;
|
|
15
|
+
private worktreeWatcher: FSWatcher | null = null;
|
|
16
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
+
private lastKnownBranch = '';
|
|
18
|
+
private lastKnownWorktreeBranches = new Map<string, string>();
|
|
19
|
+
private started = false;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly workingDir: string,
|
|
23
|
+
private readonly ctx: HandlerContext,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
start(): void {
|
|
27
|
+
if (this.started) return;
|
|
28
|
+
|
|
29
|
+
const gitDir = join(this.workingDir, '.git');
|
|
30
|
+
if (!existsSync(gitDir)) return;
|
|
31
|
+
|
|
32
|
+
this.initLastKnownBranches();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
this.headWatcher = watch(join(gitDir, 'HEAD'), () => this.debounce());
|
|
36
|
+
} catch { /* not a git repo or permission issue */ }
|
|
37
|
+
|
|
38
|
+
this.startWorktreeWatcher();
|
|
39
|
+
this.started = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
startWorktreeWatcher(): void {
|
|
43
|
+
if (this.worktreeWatcher) return;
|
|
44
|
+
try {
|
|
45
|
+
const worktreesDir = join(this.workingDir, '.git', 'worktrees');
|
|
46
|
+
if (!existsSync(worktreesDir)) return;
|
|
47
|
+
this.worktreeWatcher = watch(worktreesDir, { recursive: true }, (_event, filename) => {
|
|
48
|
+
if (filename?.endsWith('HEAD')) this.debounce();
|
|
49
|
+
});
|
|
50
|
+
} catch { /* recursive watch not supported or dir missing */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
stop(): void {
|
|
54
|
+
if (this.headWatcher) { this.headWatcher.close(); this.headWatcher = null; }
|
|
55
|
+
if (this.worktreeWatcher) { this.worktreeWatcher.close(); this.worktreeWatcher = null; }
|
|
56
|
+
if (this.debounceTimer) { clearTimeout(this.debounceTimer); this.debounceTimer = null; }
|
|
57
|
+
this.started = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async initLastKnownBranches(): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
const result = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], this.workingDir);
|
|
63
|
+
if (result.exitCode === 0) this.lastKnownBranch = result.stdout.trim();
|
|
64
|
+
} catch { /* ignore */ }
|
|
65
|
+
|
|
66
|
+
for (const [tabId, wtPath] of this.ctx.gitDirectories) {
|
|
67
|
+
if (wtPath === this.workingDir) continue;
|
|
68
|
+
try {
|
|
69
|
+
const result = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], wtPath);
|
|
70
|
+
if (result.exitCode === 0) this.lastKnownWorktreeBranches.set(tabId, result.stdout.trim());
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private debounce(): void {
|
|
76
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
77
|
+
this.debounceTimer = setTimeout(() => { this.handleChange(); }, 300);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async handleChange(): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
await this.checkMainBranch();
|
|
83
|
+
await this.checkWorktreeBranches();
|
|
84
|
+
} catch { /* ignore errors from concurrent git operations */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async checkMainBranch(): Promise<void> {
|
|
88
|
+
const result = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], this.workingDir);
|
|
89
|
+
if (result.exitCode !== 0) return;
|
|
90
|
+
const branch = result.stdout.trim();
|
|
91
|
+
if (!branch || branch === this.lastKnownBranch) return;
|
|
92
|
+
this.lastKnownBranch = branch;
|
|
93
|
+
this.ctx.broadcastToAll({
|
|
94
|
+
type: 'gitBranchChanged',
|
|
95
|
+
data: { directory: this.workingDir, branch },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async checkWorktreeBranches(): Promise<void> {
|
|
100
|
+
for (const [tabId, wtPath] of this.ctx.gitDirectories) {
|
|
101
|
+
if (wtPath === this.workingDir) continue;
|
|
102
|
+
try {
|
|
103
|
+
const result = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], wtPath);
|
|
104
|
+
if (result.exitCode !== 0) continue;
|
|
105
|
+
const branch = result.stdout.trim();
|
|
106
|
+
if (!branch) continue;
|
|
107
|
+
const lastBranch = this.lastKnownWorktreeBranches.get(tabId);
|
|
108
|
+
if (branch === lastBranch) continue;
|
|
109
|
+
this.lastKnownWorktreeBranches.set(tabId, branch);
|
|
110
|
+
this.ctx.gitBranches.set(tabId, branch);
|
|
111
|
+
const registry = this.ctx.getRegistry(this.workingDir);
|
|
112
|
+
registry.updateTabWorktree(tabId, wtPath, branch);
|
|
113
|
+
this.ctx.broadcastToAll({
|
|
114
|
+
type: 'gitBranchChanged',
|
|
115
|
+
data: { directory: wtPath, branch },
|
|
116
|
+
});
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -397,26 +397,44 @@ async function cleanupAfterMerge(
|
|
|
397
397
|
strategy: string,
|
|
398
398
|
deleteWorktree: boolean,
|
|
399
399
|
deleteBranch: boolean,
|
|
400
|
-
): Promise<
|
|
400
|
+
): Promise<{ warnings: string[]; removedWorktreePath: string | null }> {
|
|
401
|
+
const warnings: string[] = [];
|
|
402
|
+
let removedWorktreePath: string | null = null;
|
|
403
|
+
|
|
401
404
|
if (deleteWorktree) {
|
|
402
405
|
const wtList = await executeGitCommand(['worktree', 'list', '--porcelain'], mainPath);
|
|
403
406
|
const worktreePath = findWorktreePathForBranch(wtList.stdout, sourceBranch);
|
|
404
407
|
if (worktreePath && worktreePath !== mainPath) {
|
|
405
|
-
await executeGitCommand(['worktree', 'remove', worktreePath], mainPath);
|
|
408
|
+
const removeResult = await executeGitCommand(['worktree', 'remove', worktreePath], mainPath);
|
|
409
|
+
if (removeResult.exitCode !== 0) {
|
|
410
|
+
const forceResult = await executeGitCommand(['worktree', 'remove', '--force', worktreePath], mainPath);
|
|
411
|
+
if (forceResult.exitCode !== 0) {
|
|
412
|
+
warnings.push(`Failed to remove worktree: ${forceResult.stderr || 'unknown error'}`);
|
|
413
|
+
} else {
|
|
414
|
+
removedWorktreePath = worktreePath;
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
removedWorktreePath = worktreePath;
|
|
418
|
+
}
|
|
406
419
|
}
|
|
407
420
|
}
|
|
408
421
|
if (deleteBranch) {
|
|
409
422
|
const deleteFlag = strategy === 'squash' ? '-D' : '-d';
|
|
410
|
-
await executeGitCommand(['branch', deleteFlag, sourceBranch], mainPath);
|
|
423
|
+
const branchResult = await executeGitCommand(['branch', deleteFlag, sourceBranch], mainPath);
|
|
424
|
+
if (branchResult.exitCode !== 0) {
|
|
425
|
+
warnings.push(`Failed to delete branch: ${branchResult.stderr || 'unknown error'}`);
|
|
426
|
+
}
|
|
411
427
|
}
|
|
412
428
|
await executeGitCommand(['worktree', 'prune'], mainPath);
|
|
429
|
+
return { warnings, removedWorktreePath };
|
|
413
430
|
}
|
|
414
431
|
|
|
415
432
|
function findWorktreePathForBranch(porcelainOutput: string, branchName: string): string | null {
|
|
416
433
|
let currentWtPath = '';
|
|
434
|
+
const fullRef = `refs/heads/${branchName}`;
|
|
417
435
|
for (const line of porcelainOutput.split('\n')) {
|
|
418
436
|
if (line.startsWith('worktree ')) currentWtPath = line.slice(9).trim();
|
|
419
|
-
if (line.startsWith('branch ') && line.
|
|
437
|
+
if (line.startsWith('branch ') && line.slice(7).trim() === fullRef) return currentWtPath;
|
|
420
438
|
}
|
|
421
439
|
return null;
|
|
422
440
|
}
|
|
@@ -437,6 +455,8 @@ async function handleGitWorktreeMerge(ctx: HandlerContext, ws: WSContext, msg: W
|
|
|
437
455
|
return;
|
|
438
456
|
}
|
|
439
457
|
|
|
458
|
+
const headBefore = await executeGitCommand(['rev-parse', 'HEAD'], mainPath);
|
|
459
|
+
|
|
440
460
|
const mergeResult = await executeMergeStrategy(strategy, sourceBranch, commitMessage, mainPath);
|
|
441
461
|
if (mergeResult.exitCode !== 0) {
|
|
442
462
|
const conflictFiles = await detectMergeConflicts(mainPath);
|
|
@@ -447,10 +467,24 @@ async function handleGitWorktreeMerge(ctx: HandlerContext, ws: WSContext, msg: W
|
|
|
447
467
|
return;
|
|
448
468
|
}
|
|
449
469
|
|
|
470
|
+
const headAfter = await executeGitCommand(['rev-parse', 'HEAD'], mainPath);
|
|
471
|
+
if (headBefore.stdout.trim() === headAfter.stdout.trim()) {
|
|
472
|
+
ctx.send(ws, { type: 'gitWorktreeMergeResult', tabId, data: { success: false, error: `Already up to date — "${sourceBranch}" has no new commits to merge into "${targetBranch}"` } });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
450
476
|
const commitHashResult = await executeGitCommand(['rev-parse', '--short', 'HEAD'], mainPath);
|
|
451
|
-
await cleanupAfterMerge(mainPath, sourceBranch, strategy, !!deleteWorktree, !!deleteBranch);
|
|
477
|
+
const { warnings, removedWorktreePath } = await cleanupAfterMerge(mainPath, sourceBranch, strategy, !!deleteWorktree, !!deleteBranch);
|
|
452
478
|
|
|
453
|
-
|
|
479
|
+
if (removedWorktreePath) {
|
|
480
|
+
cleanupWorktreeReferences(ctx, workingDir, removedWorktreePath);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const data: Record<string, unknown> = { success: true, mergeCommit: commitHashResult.stdout.trim() };
|
|
484
|
+
if (warnings.length > 0) {
|
|
485
|
+
data.warnings = warnings;
|
|
486
|
+
}
|
|
487
|
+
ctx.send(ws, { type: 'gitWorktreeMergeResult', tabId, data });
|
|
454
488
|
} catch (error: unknown) {
|
|
455
489
|
ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
|
|
456
490
|
}
|
|
@@ -5,6 +5,7 @@ import type { ChildProcess } from 'node:child_process';
|
|
|
5
5
|
import type { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
6
6
|
import type { AutocompleteService } from './autocomplete.js';
|
|
7
7
|
import type { FileUploadHandler } from './file-upload-handler.js';
|
|
8
|
+
import type { GitHeadWatcher } from './git-head-watcher.js';
|
|
8
9
|
import type { SessionRegistry } from './session-registry.js';
|
|
9
10
|
import type { WebSocketResponse, WSContext } from './types.js';
|
|
10
11
|
|
|
@@ -33,6 +34,7 @@ export interface HandlerContext {
|
|
|
33
34
|
autocompleteService: AutocompleteService;
|
|
34
35
|
usageReporter: UsageReporter | null;
|
|
35
36
|
fileUploadHandler: FileUploadHandler | null;
|
|
37
|
+
gitHeadWatcher: GitHeadWatcher | null;
|
|
36
38
|
|
|
37
39
|
// Registry access
|
|
38
40
|
getRegistry(workingDir: string): SessionRegistry;
|
|
@@ -19,6 +19,7 @@ import { handleDeployMessage } from './deploy-handlers.js';
|
|
|
19
19
|
import { handleFileExplorerMessage, handleFileMessage } from './file-explorer-handlers.js';
|
|
20
20
|
import { FileUploadHandler } from './file-upload-handler.js';
|
|
21
21
|
import { handleGitMessage } from './git-handlers.js';
|
|
22
|
+
import { GitHeadWatcher } from './git-head-watcher.js';
|
|
22
23
|
import type { HandlerContext, UsageReporter } from './handler-context.js';
|
|
23
24
|
import { handlePlanMessage } from './plan-handlers.js';
|
|
24
25
|
import { handleQualityMessage } from './quality-handlers.js';
|
|
@@ -46,6 +47,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
46
47
|
terminalListenerCleanups: Map<string, () => void> = new Map();
|
|
47
48
|
terminalSubscribers: Map<string, Set<WSContext>> = new Map();
|
|
48
49
|
fileUploadHandler: FileUploadHandler | null = null;
|
|
50
|
+
gitHeadWatcher: GitHeadWatcher | null = null;
|
|
49
51
|
|
|
50
52
|
constructor() {
|
|
51
53
|
this.frecencyPath = join(homedir(), '.mstro', 'autocomplete-frecency.json');
|
|
@@ -103,9 +105,14 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
handleConnection(ws: WSContext,
|
|
108
|
+
handleConnection(ws: WSContext, workingDir: string): void {
|
|
107
109
|
this.connections.set(ws, new Map());
|
|
108
110
|
this.allConnections.add(ws);
|
|
111
|
+
|
|
112
|
+
if (!this.gitHeadWatcher && workingDir) {
|
|
113
|
+
this.gitHeadWatcher = new GitHeadWatcher(workingDir, this);
|
|
114
|
+
this.gitHeadWatcher.start();
|
|
115
|
+
}
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
async handleMessage(
|
|
@@ -147,7 +154,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
147
154
|
// Quality
|
|
148
155
|
qualityDetectTools: 'quality', qualityScan: 'quality', qualityInstallTools: 'quality', qualityCodeReview: 'quality', qualityFixIssues: 'quality', qualityLoadState: 'quality', qualitySaveDirectories: 'quality',
|
|
149
156
|
// Plan + boards + sprints
|
|
150
|
-
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',
|
|
157
|
+
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',
|
|
151
158
|
// File upload
|
|
152
159
|
fileUploadStart: 'fileUpload', fileUploadChunk: 'fileUpload', fileUploadComplete: 'fileUpload', fileUploadCancel: 'fileUpload',
|
|
153
160
|
// Deploy management + HTTP relay
|
|
@@ -243,10 +250,16 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
243
250
|
this.allConnections.delete(ws);
|
|
244
251
|
cleanupTerminalSubscribers(this, ws);
|
|
245
252
|
|
|
246
|
-
// Clean up
|
|
247
|
-
if (this.allConnections.size === 0
|
|
248
|
-
this.fileUploadHandler
|
|
249
|
-
|
|
253
|
+
// Clean up resources when no connections remain
|
|
254
|
+
if (this.allConnections.size === 0) {
|
|
255
|
+
if (this.fileUploadHandler) {
|
|
256
|
+
this.fileUploadHandler.destroy();
|
|
257
|
+
this.fileUploadHandler = null;
|
|
258
|
+
}
|
|
259
|
+
if (this.gitHeadWatcher) {
|
|
260
|
+
this.gitHeadWatcher.stop();
|
|
261
|
+
this.gitHeadWatcher = null;
|
|
262
|
+
}
|
|
250
263
|
}
|
|
251
264
|
}
|
|
252
265
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { handlePlanPrompt } from '../plan/composer.js';
|
|
6
7
|
import { replaceFrontMatterField } from '../plan/front-matter.js';
|
|
7
8
|
import { getNextBoardId, getNextBoardNumber, parseBoardArtifacts, parseBoardDirectory, parsePlanDirectory, resolvePmDir } from '../plan/parser.js';
|
|
8
9
|
import type { Workspace } from '../plan/types.js';
|
|
@@ -309,6 +310,121 @@ export function handleGetBoardArtifacts(
|
|
|
309
310
|
ctx.send(ws, { type: 'planBoardArtifacts', data: artifacts });
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Chat-to-board: create board from conversation and run prompt
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
export function handleChatToBoard(
|
|
318
|
+
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
319
|
+
workingDir: string, permission?: 'view',
|
|
320
|
+
): void {
|
|
321
|
+
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
322
|
+
|
|
323
|
+
const { conversation, autoImplement, focusHint } = (msg.data || {}) as {
|
|
324
|
+
conversation?: string;
|
|
325
|
+
autoImplement?: boolean;
|
|
326
|
+
focusHint?: string;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (!conversation) {
|
|
330
|
+
ctx.send(ws, { type: 'planError', data: { error: 'Conversation text is required' } });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const pmDir = resolvePmDir(workingDir);
|
|
335
|
+
if (!pmDir) {
|
|
336
|
+
ctx.send(ws, { type: 'planError', data: { error: 'No PM directory found. Run planScaffold first.' } });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const fullState = parsePlanDirectory(workingDir);
|
|
341
|
+
if (!fullState) {
|
|
342
|
+
ctx.send(ws, { type: 'planError', data: { error: 'Failed to parse PM directory' } });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const boardId = getNextBoardId(fullState.boards);
|
|
347
|
+
const boardNum = getNextBoardNumber(fullState.boards);
|
|
348
|
+
const title = `Board ${boardNum}`;
|
|
349
|
+
const boardDir = join(pmDir, 'boards', boardId);
|
|
350
|
+
|
|
351
|
+
for (const dir of ['backlog', 'out', 'reviews', 'logs']) {
|
|
352
|
+
mkdirSync(join(boardDir, dir), { recursive: true });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const today = new Date().toISOString().split('T')[0];
|
|
356
|
+
const goalLine = focusHint || 'Generated from chat conversation';
|
|
357
|
+
|
|
358
|
+
writeFileSync(join(boardDir, 'board.md'), `---
|
|
359
|
+
id: ${boardId}
|
|
360
|
+
title: "${title}"
|
|
361
|
+
status: draft
|
|
362
|
+
created: "${today}"
|
|
363
|
+
completed_at: null
|
|
364
|
+
goal: "${goalLine.replace(/"/g, '\\"')}"
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
# ${title}
|
|
368
|
+
|
|
369
|
+
## Goal
|
|
370
|
+
${goalLine}
|
|
371
|
+
|
|
372
|
+
## Notes
|
|
373
|
+
`, 'utf-8');
|
|
374
|
+
|
|
375
|
+
writeFileSync(join(boardDir, 'STATE.md'), `---
|
|
376
|
+
project: ../../project.md
|
|
377
|
+
board: board.md
|
|
378
|
+
paused: false
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
# Board State
|
|
382
|
+
|
|
383
|
+
## Ready to Work
|
|
384
|
+
|
|
385
|
+
## In Progress
|
|
386
|
+
|
|
387
|
+
## Blocked
|
|
388
|
+
|
|
389
|
+
## Recently Completed
|
|
390
|
+
|
|
391
|
+
## Warnings
|
|
392
|
+
`, 'utf-8');
|
|
393
|
+
|
|
394
|
+
writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
|
|
395
|
+
|
|
396
|
+
const wsPath = join(pmDir, 'workspace.json');
|
|
397
|
+
if (!existsSync(wsPath)) {
|
|
398
|
+
writeFileSync(wsPath, JSON.stringify({ activeBoardId: null, boardOrder: [] }, null, 2), 'utf-8');
|
|
399
|
+
}
|
|
400
|
+
const workspace: Workspace = JSON.parse(readFileSync(wsPath, 'utf-8'));
|
|
401
|
+
workspace.boardOrder.push(boardId);
|
|
402
|
+
workspace.activeBoardId = boardId;
|
|
403
|
+
writeFileSync(wsPath, JSON.stringify(workspace, null, 2), 'utf-8');
|
|
404
|
+
|
|
405
|
+
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
406
|
+
if (boardState) {
|
|
407
|
+
ctx.broadcastToAll({ type: 'planBoardCreated', data: boardState.board });
|
|
408
|
+
ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
ctx.send(ws, {
|
|
412
|
+
type: 'chatToBoardCreated',
|
|
413
|
+
data: { boardId, autoImplement: !!autoImplement },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
let prompt = conversation;
|
|
417
|
+
if (focusHint) {
|
|
418
|
+
prompt = `Focus on: ${focusHint}\n\n${conversation}`;
|
|
419
|
+
}
|
|
420
|
+
handlePlanPrompt(ctx, ws, prompt, workingDir, boardId).catch(error => {
|
|
421
|
+
ctx.send(ws, {
|
|
422
|
+
type: 'planError',
|
|
423
|
+
data: { error: error instanceof Error ? error.message : String(error) },
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
312
428
|
// ── Private helpers ──────────────────────────────────────────────────
|
|
313
429
|
|
|
314
430
|
/** Build a board-level review-custom agent file from user-provided criteria. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { HandlerContext } from './handler-context.js';
|
|
12
|
-
import { handleArchiveBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
12
|
+
import { handleArchiveBoard, handleChatToBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
13
13
|
import { handleExecute, handleExecuteEpic, handlePause, handlePrompt, handleResume, handleStop } from './plan-execution-handlers.js';
|
|
14
14
|
import { handleCreateIssue, handleDeleteIssue, handleGetIssue, handleGetMilestone, handleGetSprint, handleListIssues, handlePlanInit, handleScaffold, handleUpdateIssue } from './plan-issue-handlers.js';
|
|
15
15
|
import { handleActivateSprint, handleCompleteSprint, handleCreateSprint, handleGetSprintArtifacts } from './plan-sprint-handlers.js';
|
|
@@ -52,6 +52,8 @@ export function handlePlanMessage(
|
|
|
52
52
|
planReorderBoards: () => handleReorderBoards(ctx, ws, msg, workingDir, permission),
|
|
53
53
|
planSetActiveBoard: () => handleSetActiveBoard(ctx, ws, msg, workingDir, permission),
|
|
54
54
|
planGetBoardArtifacts: () => handleGetBoardArtifacts(ctx, ws, msg, workingDir),
|
|
55
|
+
// Chat-to-board (from /board and /ship skills)
|
|
56
|
+
chatToBoard: () => handleChatToBoard(ctx, ws, msg, workingDir, permission),
|
|
55
57
|
// Sprint lifecycle (legacy)
|
|
56
58
|
planCreateSprint: () => handleCreateSprint(ctx, ws, msg, workingDir, permission),
|
|
57
59
|
planActivateSprint: () => handleActivateSprint(ctx, ws, msg, workingDir, permission),
|
|
@@ -34,10 +34,18 @@ export interface HistoryDirectoryEntry {
|
|
|
34
34
|
grade: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface HistoryCategoryScore {
|
|
38
|
+
category: string;
|
|
39
|
+
score: number;
|
|
40
|
+
grade: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
export interface QualityHistoryEntry {
|
|
38
44
|
timestamp: string;
|
|
39
45
|
overall: number;
|
|
40
46
|
grade: string;
|
|
47
|
+
issueDensity?: number;
|
|
48
|
+
categoryScores?: HistoryCategoryScore[];
|
|
41
49
|
directories: HistoryDirectoryEntry[];
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -187,12 +195,10 @@ export class QualityPersistence {
|
|
|
187
195
|
appendHistory(results: QualityResults, dirPath: string): void {
|
|
188
196
|
const history = this.loadHistory();
|
|
189
197
|
|
|
190
|
-
// Find or create entry for this timestamp batch
|
|
191
|
-
// If the last entry was within 60 seconds, merge into it (for multi-dir scans)
|
|
192
198
|
const now = new Date();
|
|
193
199
|
const lastEntry = history[history.length - 1];
|
|
194
200
|
const lastTime = lastEntry ? new Date(lastEntry.timestamp).getTime() : 0;
|
|
195
|
-
const mergeWindow = 60_000;
|
|
201
|
+
const mergeWindow = 60_000;
|
|
196
202
|
|
|
197
203
|
const dirEntry: HistoryDirectoryEntry = {
|
|
198
204
|
path: dirPath,
|
|
@@ -200,30 +206,40 @@ export class QualityPersistence {
|
|
|
200
206
|
grade: results.grade,
|
|
201
207
|
};
|
|
202
208
|
|
|
209
|
+
const categoryScores: HistoryCategoryScore[] | undefined = results.scoreBreakdown
|
|
210
|
+
? results.scoreBreakdown.categoryPenalties.map((cp) => ({
|
|
211
|
+
category: cp.category,
|
|
212
|
+
score: cp.score,
|
|
213
|
+
grade: cp.grade,
|
|
214
|
+
}))
|
|
215
|
+
: undefined;
|
|
216
|
+
|
|
217
|
+
const issueDensity = results.scoreBreakdown?.issueDensity;
|
|
218
|
+
|
|
203
219
|
if (lastEntry && now.getTime() - lastTime < mergeWindow) {
|
|
204
|
-
// Merge: update or add this directory in the last entry
|
|
205
220
|
const existing = lastEntry.directories.findIndex((d) => d.path === dirPath);
|
|
206
221
|
if (existing >= 0) {
|
|
207
222
|
lastEntry.directories[existing] = dirEntry;
|
|
208
223
|
} else {
|
|
209
224
|
lastEntry.directories.push(dirEntry);
|
|
210
225
|
}
|
|
211
|
-
// Recompute overall as average of all directories in this entry
|
|
212
226
|
const totalScore = lastEntry.directories.reduce((sum, d) => sum + d.score, 0);
|
|
213
227
|
lastEntry.overall = Math.round(totalScore / lastEntry.directories.length);
|
|
214
228
|
lastEntry.grade = gradeFromScore(lastEntry.overall);
|
|
215
229
|
lastEntry.timestamp = now.toISOString();
|
|
230
|
+
if (categoryScores) lastEntry.categoryScores = categoryScores;
|
|
231
|
+
if (issueDensity !== undefined) lastEntry.issueDensity = issueDensity;
|
|
216
232
|
} else {
|
|
217
|
-
// New entry
|
|
218
233
|
history.push({
|
|
219
234
|
timestamp: now.toISOString(),
|
|
220
235
|
overall: results.overall,
|
|
221
236
|
grade: results.grade,
|
|
237
|
+
issueDensity,
|
|
238
|
+
categoryScores,
|
|
222
239
|
directories: [dirEntry],
|
|
223
240
|
});
|
|
224
241
|
}
|
|
225
242
|
|
|
226
|
-
// Trim to max entries
|
|
227
243
|
while (history.length > MAX_HISTORY_ENTRIES) {
|
|
228
244
|
history.shift();
|
|
229
245
|
}
|
|
@@ -443,18 +443,8 @@ function persistReviewResults(
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
let updatedResults: import('./quality-service.js').QualityResults;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
...existingReport,
|
|
449
|
-
overall: reviewResult.score,
|
|
450
|
-
grade: reviewResult.grade,
|
|
451
|
-
codeReview: findings,
|
|
452
|
-
scoreRationale: reviewResult.scoreRationale ?? undefined,
|
|
453
|
-
};
|
|
454
|
-
} else {
|
|
455
|
-
updatedResults = recomputeWithAiReview(existingReport, reviewResult.findings);
|
|
456
|
-
updatedResults = { ...updatedResults, codeReview: findings };
|
|
457
|
-
}
|
|
446
|
+
updatedResults = recomputeWithAiReview(existingReport, reviewResult.findings);
|
|
447
|
+
updatedResults = { ...updatedResults, codeReview: findings };
|
|
458
448
|
|
|
459
449
|
persistence.saveReport(reportPath, updatedResults);
|
|
460
450
|
persistence.appendHistory(updatedResults, reportPath);
|