scalix 1.0.0 → 1.2.0
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/.env.example +29 -0
- package/PARITY-PLAN.md +116 -0
- package/README.md +1 -1
- package/bin/scalix.js +1 -0
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent.js +363 -74
- package/dist/api-client.js +77 -0
- package/dist/cli.js +724 -76
- package/dist/commands.js +53 -2
- package/dist/constants.js +43 -9
- package/dist/context-builder.js +8 -1
- package/dist/context-retriever.js +257 -0
- package/dist/git-context.js +32 -29
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +149 -20
- package/dist/hooks.js +148 -10
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +6 -2
- package/dist/keychain.js +109 -0
- package/dist/markdown.js +46 -3
- package/dist/plugins.js +132 -0
- package/dist/project-index.js +22 -1
- package/dist/sandbox.js +201 -11
- package/dist/sessions.js +96 -2
- package/dist/settings.js +194 -8
- package/dist/skills.js +269 -0
- package/dist/sub-agent.js +83 -8
- package/dist/task-board.js +105 -0
- package/dist/terminal-notify.js +73 -0
- package/dist/text-utils.js +146 -0
- package/dist/tools-advanced.js +304 -49
- package/dist/tools-browser.js +280 -19
- package/dist/tools-extra.js +9 -8
- package/dist/tools-vision.js +4 -3
- package/dist/tools.js +13 -3
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/package.json +8 -3
- package/app.py +0 -55
package/dist/skills.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
import { PROJECT_LOOKUP_DIRS } from './constants.js';
|
|
6
|
+
const SKILL_DIRS = PROJECT_LOOKUP_DIRS.map(d => `${d}/skills`);
|
|
7
|
+
function parseSkillFrontmatter(content) {
|
|
8
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
9
|
+
if (!match)
|
|
10
|
+
return null;
|
|
11
|
+
const meta = {};
|
|
12
|
+
let currentKey = '';
|
|
13
|
+
let isList = false;
|
|
14
|
+
for (const line of match[1].split('\n')) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (trimmed.startsWith('- ') && isList && currentKey) {
|
|
17
|
+
const arr = meta[currentKey];
|
|
18
|
+
if (Array.isArray(arr))
|
|
19
|
+
arr.push(trimmed.slice(2).trim());
|
|
20
|
+
else
|
|
21
|
+
meta[currentKey] = [trimmed.slice(2).trim()];
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const idx = line.indexOf(':');
|
|
25
|
+
if (idx > 0) {
|
|
26
|
+
currentKey = line.slice(0, idx).trim();
|
|
27
|
+
const value = line.slice(idx + 1).trim();
|
|
28
|
+
if (value === '' || value === '[]') {
|
|
29
|
+
meta[currentKey] = [];
|
|
30
|
+
isList = true;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
meta[currentKey] = value;
|
|
34
|
+
isList = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const name = meta.name || '';
|
|
39
|
+
const description = meta.description || '';
|
|
40
|
+
if (!name)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
meta: {
|
|
44
|
+
name,
|
|
45
|
+
description,
|
|
46
|
+
version: meta.version || '1.0.0',
|
|
47
|
+
paths: Array.isArray(meta.paths) ? meta.paths : (meta.paths ? [meta.paths] : []),
|
|
48
|
+
triggers: Array.isArray(meta.triggers) ? meta.triggers : (meta.triggers ? [meta.triggers] : []),
|
|
49
|
+
},
|
|
50
|
+
body: match[2],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function discoverResources(skillDir) {
|
|
54
|
+
const resources = { scripts: [], references: [], examples: [], assets: [] };
|
|
55
|
+
const dirs = ['scripts', 'references', 'examples', 'assets'];
|
|
56
|
+
for (const dir of dirs) {
|
|
57
|
+
const fullDir = path.join(skillDir, dir);
|
|
58
|
+
try {
|
|
59
|
+
const entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
60
|
+
resources[dir] = entries
|
|
61
|
+
.filter(e => e.isFile())
|
|
62
|
+
.map(e => path.join(fullDir, e.name));
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
return resources;
|
|
67
|
+
}
|
|
68
|
+
export async function loadSkills(cwd = process.cwd()) {
|
|
69
|
+
const skills = [];
|
|
70
|
+
for (const dir of SKILL_DIRS) {
|
|
71
|
+
const fullDir = path.resolve(cwd, dir);
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!entry.isDirectory()) {
|
|
81
|
+
if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
82
|
+
const skill = await loadSkillFile(path.join(fullDir, entry.name), fullDir);
|
|
83
|
+
if (skill)
|
|
84
|
+
skills.push(skill);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const skillFile = path.join(fullDir, entry.name, 'SKILL.md');
|
|
89
|
+
if (existsSync(skillFile)) {
|
|
90
|
+
const skill = await loadSkillFile(skillFile, path.join(fullDir, entry.name));
|
|
91
|
+
if (skill)
|
|
92
|
+
skills.push(skill);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
skills.push(...getBuiltinSkills());
|
|
97
|
+
return skills;
|
|
98
|
+
}
|
|
99
|
+
async function loadSkillFile(filePath, skillDir) {
|
|
100
|
+
try {
|
|
101
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
102
|
+
const parsed = parseSkillFrontmatter(raw);
|
|
103
|
+
if (!parsed)
|
|
104
|
+
return null;
|
|
105
|
+
const resources = await discoverResources(skillDir);
|
|
106
|
+
return {
|
|
107
|
+
name: parsed.meta.name,
|
|
108
|
+
description: parsed.meta.description,
|
|
109
|
+
version: parsed.meta.version || '1.0.0',
|
|
110
|
+
paths: parsed.meta.paths || [],
|
|
111
|
+
triggers: parsed.meta.triggers || [],
|
|
112
|
+
content: parsed.body.trim(),
|
|
113
|
+
source: filePath,
|
|
114
|
+
resources,
|
|
115
|
+
builtin: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
logger.warn('Failed to load skill', { path: filePath, error: err.message });
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function getSkillsForPath(skills, filePath) {
|
|
124
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
125
|
+
return skills.filter(skill => skill.paths.some(p => {
|
|
126
|
+
const pattern = p.replace(/\\/g, '/');
|
|
127
|
+
if (pattern.endsWith('/**')) {
|
|
128
|
+
return normalized.startsWith(pattern.slice(0, -3));
|
|
129
|
+
}
|
|
130
|
+
if (pattern.endsWith('/*')) {
|
|
131
|
+
const dir = pattern.slice(0, -2);
|
|
132
|
+
return normalized.startsWith(dir) && !normalized.slice(dir.length + 1).includes('/');
|
|
133
|
+
}
|
|
134
|
+
return normalized.startsWith(pattern);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
export function getSkillByTrigger(skills, input) {
|
|
138
|
+
const lower = input.toLowerCase();
|
|
139
|
+
return skills.find(skill => skill.triggers.some(t => lower.includes(t.toLowerCase()))) || null;
|
|
140
|
+
}
|
|
141
|
+
export function getSkillContent(skill, level = 'full') {
|
|
142
|
+
if (level === 'metadata') {
|
|
143
|
+
return `**${skill.name}** (v${skill.version}) — ${skill.description}`;
|
|
144
|
+
}
|
|
145
|
+
let content = skill.content;
|
|
146
|
+
if (level === 'resources') {
|
|
147
|
+
const resourceFiles = [
|
|
148
|
+
...skill.resources.scripts,
|
|
149
|
+
...skill.resources.references,
|
|
150
|
+
...skill.resources.examples,
|
|
151
|
+
];
|
|
152
|
+
for (const filePath of resourceFiles) {
|
|
153
|
+
if (existsSync(filePath)) {
|
|
154
|
+
try {
|
|
155
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
156
|
+
const relPath = path.basename(filePath);
|
|
157
|
+
content += `\n\n<resource path="${relPath}">\n${fileContent}\n</resource>`;
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return content;
|
|
164
|
+
}
|
|
165
|
+
export function skillsSummary(skills) {
|
|
166
|
+
if (skills.length === 0)
|
|
167
|
+
return 'No skills available.';
|
|
168
|
+
const lines = ['Available skills:'];
|
|
169
|
+
for (const s of skills) {
|
|
170
|
+
const paths = s.paths.length > 0 ? ` [paths: ${s.paths.join(', ')}]` : '';
|
|
171
|
+
const builtin = s.builtin ? ' (built-in)' : '';
|
|
172
|
+
lines.push(` ${s.name} — ${s.description}${paths}${builtin}`);
|
|
173
|
+
}
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
function getBuiltinSkills() {
|
|
177
|
+
const emptyResources = { scripts: [], references: [], examples: [], assets: [] };
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
name: 'feature-dev',
|
|
181
|
+
description: 'Guided 7-phase feature development workflow',
|
|
182
|
+
version: '1.0.0',
|
|
183
|
+
paths: [],
|
|
184
|
+
triggers: ['new feature', 'implement feature', 'build feature', 'add feature'],
|
|
185
|
+
content: `# Feature Development Workflow
|
|
186
|
+
|
|
187
|
+
Follow this 7-phase process for developing new features:
|
|
188
|
+
|
|
189
|
+
## Phase 1: Requirements
|
|
190
|
+
- Clarify the feature scope and acceptance criteria
|
|
191
|
+
- Identify affected files and components
|
|
192
|
+
- List edge cases and error scenarios
|
|
193
|
+
|
|
194
|
+
## Phase 2: Design
|
|
195
|
+
- Propose the technical approach
|
|
196
|
+
- Identify new files, modified files, and interfaces
|
|
197
|
+
- Consider backwards compatibility
|
|
198
|
+
|
|
199
|
+
## Phase 3: Implementation
|
|
200
|
+
- Write the core implementation
|
|
201
|
+
- Follow existing code conventions
|
|
202
|
+
- Keep changes minimal and focused
|
|
203
|
+
|
|
204
|
+
## Phase 4: Testing
|
|
205
|
+
- Write unit tests for new functions
|
|
206
|
+
- Test edge cases identified in Phase 1
|
|
207
|
+
- Run existing test suite to check for regressions
|
|
208
|
+
|
|
209
|
+
## Phase 5: Integration
|
|
210
|
+
- Wire up the feature with existing code
|
|
211
|
+
- Update configuration if needed
|
|
212
|
+
- Add feature flags if appropriate
|
|
213
|
+
|
|
214
|
+
## Phase 6: Documentation
|
|
215
|
+
- Update relevant documentation
|
|
216
|
+
- Add inline comments only where the "why" is non-obvious
|
|
217
|
+
- Update CHANGELOG if one exists
|
|
218
|
+
|
|
219
|
+
## Phase 7: Review
|
|
220
|
+
- Run linter and type checker
|
|
221
|
+
- Review diff for accidental changes
|
|
222
|
+
- Verify the feature works end-to-end`,
|
|
223
|
+
source: 'built-in',
|
|
224
|
+
resources: emptyResources,
|
|
225
|
+
builtin: true,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'code-review',
|
|
229
|
+
description: 'Systematic code review of current changes',
|
|
230
|
+
version: '1.0.0',
|
|
231
|
+
paths: [],
|
|
232
|
+
triggers: ['review code', 'code review', 'review changes', 'review my changes'],
|
|
233
|
+
content: `# Code Review
|
|
234
|
+
|
|
235
|
+
Review the current changes systematically:
|
|
236
|
+
|
|
237
|
+
1. **Run \`git diff\`** to see all uncommitted changes
|
|
238
|
+
2. **Security**: Check for hardcoded secrets, SQL injection, XSS, path traversal
|
|
239
|
+
3. **Correctness**: Verify logic, edge cases, error handling
|
|
240
|
+
4. **Style**: Check naming, formatting, consistency with codebase conventions
|
|
241
|
+
5. **Performance**: Look for N+1 queries, unnecessary allocations, blocking I/O
|
|
242
|
+
6. **Testing**: Are new functions tested? Are existing tests still valid?
|
|
243
|
+
7. **Summary**: Provide a structured review with findings categorized as Critical / Important / Minor`,
|
|
244
|
+
source: 'built-in',
|
|
245
|
+
resources: emptyResources,
|
|
246
|
+
builtin: true,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'commit-push-pr',
|
|
250
|
+
description: 'Commit changes, push, and create a pull request',
|
|
251
|
+
version: '1.0.0',
|
|
252
|
+
paths: [],
|
|
253
|
+
triggers: ['commit and push', 'create pr', 'open pr', 'make a pr'],
|
|
254
|
+
content: `# Commit, Push & PR
|
|
255
|
+
|
|
256
|
+
1. **Stage changes**: Review \`git status\` and stage relevant files
|
|
257
|
+
2. **Commit**: Write a concise commit message (imperative mood, <72 chars)
|
|
258
|
+
3. **Push**: Push to the remote branch
|
|
259
|
+
4. **PR**: Create a pull request with:
|
|
260
|
+
- Clear title (matches commit message style)
|
|
261
|
+
- Description summarizing what changed and why
|
|
262
|
+
- Link to related issues if applicable
|
|
263
|
+
- List any manual testing done`,
|
|
264
|
+
source: 'built-in',
|
|
265
|
+
resources: emptyResources,
|
|
266
|
+
builtin: true,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
}
|
package/dist/sub-agent.js
CHANGED
|
@@ -5,15 +5,21 @@ import { webTools } from './tools-web.js';
|
|
|
5
5
|
import { advancedTools } from './tools-advanced.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
7
|
import { SUB_AGENT_DEFAULTS, CHEAP_MODEL_INDICATORS } from './constants.js';
|
|
8
|
+
import { AgentRegistry, generateAgentId } from './agent-registry.js';
|
|
9
|
+
import { createWorktree, removeWorktree, mergeWorktree } from './worktree.js';
|
|
8
10
|
const READ_ONLY_TOOL_NAMES = new Set([
|
|
9
11
|
'get_current_time', 'calculate', 'read_file', 'read_file_lines',
|
|
10
12
|
'list_files', 'search_files', 'find_files', 'tree',
|
|
11
13
|
'git_status', 'git_diff', 'web_search', 'monitor',
|
|
12
14
|
]);
|
|
13
|
-
function selectTools(level) {
|
|
14
|
-
// Include advanced tools but exclude 'agent' to prevent recursive spawning
|
|
15
|
+
function selectTools(level, allowAgentTool = false) {
|
|
15
16
|
const all = [...defaultTools, ...extraTools, ...webTools, ...advancedTools]
|
|
16
|
-
.filter((t) =>
|
|
17
|
+
.filter((t) => {
|
|
18
|
+
const name = t.function?.name || t.name;
|
|
19
|
+
if (name === 'agent' && !allowAgentTool)
|
|
20
|
+
return false;
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
17
23
|
switch (level) {
|
|
18
24
|
case 'read-only':
|
|
19
25
|
return all.filter((t) => READ_ONLY_TOOL_NAMES.has(t.function?.name || t.name));
|
|
@@ -26,10 +32,32 @@ function selectTools(level) {
|
|
|
26
32
|
}
|
|
27
33
|
export async function spawnSubAgent(parentConfig, prompt, options = {}) {
|
|
28
34
|
const model = options.model || parentConfig.models[parentConfig.models.length - 1];
|
|
29
|
-
const
|
|
35
|
+
const allowAgentTool = options.allowAgentTool ?? false;
|
|
36
|
+
const tools = selectTools(options.tools, allowAgentTool);
|
|
30
37
|
const timeout = options.timeout ?? SUB_AGENT_DEFAULTS.timeoutMs;
|
|
38
|
+
const subAgentId = generateAgentId();
|
|
39
|
+
const parentId = options.parentAgentId ?? null;
|
|
40
|
+
const description = options.agentDescription ?? 'sub-agent';
|
|
41
|
+
const registry = AgentRegistry.getInstance();
|
|
42
|
+
const spawnCheck = registry.canSpawn(parentId);
|
|
43
|
+
if (!spawnCheck.allowed) {
|
|
44
|
+
throw new Error(`Cannot spawn agent: ${spawnCheck.reason}`);
|
|
45
|
+
}
|
|
31
46
|
const permissionMode = options.permissionMode
|
|
32
|
-
?? (options.tools === 'read-only' ? 'plan' :
|
|
47
|
+
?? (options.tools === 'read-only' ? 'plan' : parentConfig.permissionMode ?? 'default');
|
|
48
|
+
// Worktree isolation for write-capable agents
|
|
49
|
+
const shouldIsolate = options.isolate !== false && options.tools !== 'read-only';
|
|
50
|
+
let worktreeInfo = null;
|
|
51
|
+
if (shouldIsolate) {
|
|
52
|
+
try {
|
|
53
|
+
worktreeInfo = await createWorktree(subAgentId, process.cwd());
|
|
54
|
+
logger.info('Agent isolated in worktree', { agentId: subAgentId, path: worktreeInfo.path });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.warn('Worktree creation failed, running without isolation', { error: err.message });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const isReadOnly = options.tools === 'read-only';
|
|
33
61
|
const subAgent = createAgent({
|
|
34
62
|
apiKey: parentConfig.apiKey,
|
|
35
63
|
baseUrl: parentConfig.baseUrl,
|
|
@@ -43,6 +71,11 @@ export async function spawnSubAgent(parentConfig, prompt, options = {}) {
|
|
|
43
71
|
maxContextTokens: SUB_AGENT_DEFAULTS.maxContextTokens,
|
|
44
72
|
compressionThreshold: SUB_AGENT_DEFAULTS.compressionThreshold,
|
|
45
73
|
},
|
|
74
|
+
agentId: subAgentId,
|
|
75
|
+
parentAgentId: parentId,
|
|
76
|
+
agentDescription: description,
|
|
77
|
+
groundingCheck: true,
|
|
78
|
+
contextRetrieval: !isReadOnly,
|
|
46
79
|
});
|
|
47
80
|
let steps = 0;
|
|
48
81
|
let tokenEstimate = 0;
|
|
@@ -56,18 +89,60 @@ export async function spawnSubAgent(parentConfig, prompt, options = {}) {
|
|
|
56
89
|
});
|
|
57
90
|
try {
|
|
58
91
|
const response = await Promise.race([subAgent.send(prompt), timeoutPromise]);
|
|
59
|
-
logger.info('Sub-agent completed', { model, steps, tokens: tokenEstimate });
|
|
60
|
-
|
|
92
|
+
logger.info('Sub-agent completed', { agentId: subAgentId, model, steps, tokens: tokenEstimate });
|
|
93
|
+
let mergeResult;
|
|
94
|
+
if (worktreeInfo) {
|
|
95
|
+
try {
|
|
96
|
+
mergeResult = await mergeWorktree(worktreeInfo, process.cwd());
|
|
97
|
+
logger.info('Worktree merge result', {
|
|
98
|
+
agentId: subAgentId,
|
|
99
|
+
merged: mergeResult.merged,
|
|
100
|
+
commits: mergeResult.commitCount,
|
|
101
|
+
conflicts: mergeResult.conflicts?.length ?? 0,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
logger.warn('Worktree merge failed', { error: err.message });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
response,
|
|
110
|
+
model,
|
|
111
|
+
tokenEstimate,
|
|
112
|
+
steps,
|
|
113
|
+
agentId: subAgentId,
|
|
114
|
+
worktree: mergeResult ? {
|
|
115
|
+
merged: mergeResult.merged,
|
|
116
|
+
conflicts: mergeResult.conflicts,
|
|
117
|
+
commitCount: mergeResult.commitCount,
|
|
118
|
+
} : undefined,
|
|
119
|
+
};
|
|
61
120
|
}
|
|
62
121
|
catch (err) {
|
|
63
122
|
subAgent.abort();
|
|
64
|
-
logger.error('Sub-agent failed', { model, error: err.message });
|
|
123
|
+
logger.error('Sub-agent failed', { agentId: subAgentId, model, error: err.message });
|
|
65
124
|
throw err;
|
|
66
125
|
}
|
|
67
126
|
finally {
|
|
68
127
|
clearTimeout(timer);
|
|
128
|
+
if (worktreeInfo) {
|
|
129
|
+
try {
|
|
130
|
+
await removeWorktree(worktreeInfo, process.cwd());
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logger.warn('Worktree cleanup failed', { error: err.message });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
69
136
|
}
|
|
70
137
|
}
|
|
138
|
+
export async function spawnParallelAgents(parentConfig, requests) {
|
|
139
|
+
logger.info('Spawning parallel agents', { count: requests.length });
|
|
140
|
+
return Promise.allSettled(requests.map(r => spawnSubAgent(parentConfig, r.prompt, {
|
|
141
|
+
...r.options,
|
|
142
|
+
allowAgentTool: r.options?.allowAgentTool ?? true,
|
|
143
|
+
agentDescription: r.description,
|
|
144
|
+
})));
|
|
145
|
+
}
|
|
71
146
|
export function pickCheapestModel(models) {
|
|
72
147
|
const cheapIndicators = CHEAP_MODEL_INDICATORS;
|
|
73
148
|
for (const model of [...models].reverse()) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
export class TaskBoard extends EventEmitter {
|
|
5
|
+
static instance = null;
|
|
6
|
+
tasks = new Map();
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
10
|
+
static getInstance() {
|
|
11
|
+
if (!TaskBoard.instance) {
|
|
12
|
+
TaskBoard.instance = new TaskBoard();
|
|
13
|
+
}
|
|
14
|
+
return TaskBoard.instance;
|
|
15
|
+
}
|
|
16
|
+
static resetInstance() {
|
|
17
|
+
if (TaskBoard.instance) {
|
|
18
|
+
TaskBoard.instance.removeAllListeners();
|
|
19
|
+
TaskBoard.instance.tasks.clear();
|
|
20
|
+
}
|
|
21
|
+
TaskBoard.instance = null;
|
|
22
|
+
}
|
|
23
|
+
create(agentId, title, description) {
|
|
24
|
+
const task = {
|
|
25
|
+
id: randomUUID().slice(0, 8),
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
status: 'open',
|
|
29
|
+
assignee: null,
|
|
30
|
+
createdBy: agentId,
|
|
31
|
+
createdAt: Date.now(),
|
|
32
|
+
updatedAt: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
this.tasks.set(task.id, task);
|
|
35
|
+
this.emit('task:created', task);
|
|
36
|
+
logger.debug('Task created', { id: task.id, title, by: agentId });
|
|
37
|
+
return task;
|
|
38
|
+
}
|
|
39
|
+
claim(taskId, agentId) {
|
|
40
|
+
const task = this.tasks.get(taskId);
|
|
41
|
+
if (!task) {
|
|
42
|
+
return { error: `Task "${taskId}" not found` };
|
|
43
|
+
}
|
|
44
|
+
if (task.status !== 'open') {
|
|
45
|
+
return { error: `Task "${taskId}" is not open (status: ${task.status}, assignee: ${task.assignee})` };
|
|
46
|
+
}
|
|
47
|
+
task.assignee = agentId;
|
|
48
|
+
task.status = 'claimed';
|
|
49
|
+
task.updatedAt = Date.now();
|
|
50
|
+
this.emit('task:claimed', task);
|
|
51
|
+
logger.debug('Task claimed', { id: taskId, by: agentId });
|
|
52
|
+
return task;
|
|
53
|
+
}
|
|
54
|
+
update(taskId, agentId, updates) {
|
|
55
|
+
const task = this.tasks.get(taskId);
|
|
56
|
+
if (!task) {
|
|
57
|
+
return { error: `Task "${taskId}" not found` };
|
|
58
|
+
}
|
|
59
|
+
if (task.assignee && task.assignee !== agentId && task.createdBy !== agentId) {
|
|
60
|
+
return { error: `Agent "${agentId}" is not the assignee or creator of task "${taskId}"` };
|
|
61
|
+
}
|
|
62
|
+
if (updates.status) {
|
|
63
|
+
task.status = updates.status;
|
|
64
|
+
if (updates.status === 'in_progress' && !task.assignee) {
|
|
65
|
+
task.assignee = agentId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (updates.result !== undefined) {
|
|
69
|
+
task.result = updates.result;
|
|
70
|
+
}
|
|
71
|
+
task.updatedAt = Date.now();
|
|
72
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
73
|
+
this.emit('task:completed', task);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.emit('task:updated', task);
|
|
77
|
+
}
|
|
78
|
+
logger.debug('Task updated', { id: taskId, status: task.status, by: agentId });
|
|
79
|
+
return task;
|
|
80
|
+
}
|
|
81
|
+
list(filter) {
|
|
82
|
+
let tasks = [...this.tasks.values()];
|
|
83
|
+
if (filter?.status) {
|
|
84
|
+
tasks = tasks.filter(t => t.status === filter.status);
|
|
85
|
+
}
|
|
86
|
+
if (filter?.assignee) {
|
|
87
|
+
tasks = tasks.filter(t => t.assignee === filter.assignee);
|
|
88
|
+
}
|
|
89
|
+
return tasks;
|
|
90
|
+
}
|
|
91
|
+
get(taskId) {
|
|
92
|
+
return this.tasks.get(taskId);
|
|
93
|
+
}
|
|
94
|
+
getStats() {
|
|
95
|
+
const all = [...this.tasks.values()];
|
|
96
|
+
return {
|
|
97
|
+
total: all.length,
|
|
98
|
+
open: all.filter(t => t.status === 'open').length,
|
|
99
|
+
claimed: all.filter(t => t.status === 'claimed').length,
|
|
100
|
+
inProgress: all.filter(t => t.status === 'in_progress').length,
|
|
101
|
+
completed: all.filter(t => t.status === 'completed').length,
|
|
102
|
+
failed: all.filter(t => t.status === 'failed').length,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Terminal notification passthrough for iTerm2, Kitty, Ghostty, and tmux
|
|
2
|
+
function detectTerminal() {
|
|
3
|
+
const term = process.env.TERM_PROGRAM?.toLowerCase() || '';
|
|
4
|
+
const tmux = !!process.env.TMUX;
|
|
5
|
+
if (tmux)
|
|
6
|
+
return 'tmux';
|
|
7
|
+
if (term === 'iterm.app' || term === 'iterm2')
|
|
8
|
+
return 'iterm2';
|
|
9
|
+
if (term === 'kitty')
|
|
10
|
+
return 'kitty';
|
|
11
|
+
if (term === 'ghostty')
|
|
12
|
+
return 'ghostty';
|
|
13
|
+
return 'generic';
|
|
14
|
+
}
|
|
15
|
+
function wrapForTmux(seq) {
|
|
16
|
+
return `\x1bPtmux;\x1b${seq}\x1b\\`;
|
|
17
|
+
}
|
|
18
|
+
function sendOSC(code, payload) {
|
|
19
|
+
const terminal = detectTerminal();
|
|
20
|
+
const seq = `\x1b]${code};${payload}\x07`;
|
|
21
|
+
if (terminal === 'tmux') {
|
|
22
|
+
process.stdout.write(wrapForTmux(seq));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
process.stdout.write(seq);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function sendNotification(title, body) {
|
|
29
|
+
const terminal = detectTerminal();
|
|
30
|
+
switch (terminal) {
|
|
31
|
+
case 'iterm2':
|
|
32
|
+
sendOSC(9, body ? `${title}: ${body}` : title);
|
|
33
|
+
break;
|
|
34
|
+
case 'kitty':
|
|
35
|
+
sendOSC(99, `i=1:d=0:p=title;${title}`);
|
|
36
|
+
if (body) {
|
|
37
|
+
sendOSC(99, `i=1:d=1:p=body;${body}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
sendOSC(99, `i=1:d=1:p=body;`);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case 'ghostty':
|
|
44
|
+
sendOSC(777, `notify;${title};${body || ''}`);
|
|
45
|
+
break;
|
|
46
|
+
case 'tmux': {
|
|
47
|
+
const tmuxPayload = body ? `${title}: ${body}` : title;
|
|
48
|
+
sendOSC(9, tmuxPayload);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'generic':
|
|
52
|
+
// OSC 9 is widely supported (ConEmu, Windows Terminal, etc.)
|
|
53
|
+
sendOSC(9, body ? `${title}: ${body}` : title);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function sendBell() {
|
|
58
|
+
process.stdout.write('\x07');
|
|
59
|
+
}
|
|
60
|
+
export function setTerminalTitle(title) {
|
|
61
|
+
sendOSC(0, title);
|
|
62
|
+
}
|
|
63
|
+
export function notifyTaskComplete(taskName) {
|
|
64
|
+
sendNotification('Scalix', `${taskName} complete`);
|
|
65
|
+
sendBell();
|
|
66
|
+
}
|
|
67
|
+
export function notifyError(message) {
|
|
68
|
+
sendNotification('Scalix Error', message);
|
|
69
|
+
sendBell();
|
|
70
|
+
}
|
|
71
|
+
export function getTerminalType() {
|
|
72
|
+
return detectTerminal();
|
|
73
|
+
}
|