codekin 0.4.1 → 0.5.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/README.md +12 -15
- package/bin/codekin.mjs +52 -32
- package/dist/assets/index-BwKZeT4V.css +1 -0
- package/dist/assets/index-CfBnNU24.js +186 -0
- package/dist/index.html +2 -2
- package/package.json +2 -7
- package/server/dist/approval-manager.d.ts +7 -2
- package/server/dist/approval-manager.js +44 -78
- package/server/dist/approval-manager.js.map +1 -1
- package/server/dist/claude-process.d.ts +23 -3
- package/server/dist/claude-process.js +120 -27
- package/server/dist/claude-process.js.map +1 -1
- package/server/dist/commit-event-handler.js.map +1 -1
- package/server/dist/config.d.ts +4 -0
- package/server/dist/config.js +17 -0
- package/server/dist/config.js.map +1 -1
- package/server/dist/diff-manager.d.ts +41 -0
- package/server/dist/diff-manager.js +303 -0
- package/server/dist/diff-manager.js.map +1 -0
- package/server/dist/error-page.d.ts +5 -0
- package/server/dist/error-page.js +144 -0
- package/server/dist/error-page.js.map +1 -0
- package/server/dist/native-permissions.d.ts +44 -0
- package/server/dist/native-permissions.js +163 -0
- package/server/dist/native-permissions.js.map +1 -0
- package/server/dist/orchestrator-children.d.ts +74 -0
- package/server/dist/orchestrator-children.js +281 -0
- package/server/dist/orchestrator-children.js.map +1 -0
- package/server/dist/orchestrator-learning.d.ts +134 -0
- package/server/dist/orchestrator-learning.js +567 -0
- package/server/dist/orchestrator-learning.js.map +1 -0
- package/server/dist/orchestrator-manager.d.ts +25 -0
- package/server/dist/orchestrator-manager.js +353 -0
- package/server/dist/orchestrator-manager.js.map +1 -0
- package/server/dist/orchestrator-memory.d.ts +77 -0
- package/server/dist/orchestrator-memory.js +288 -0
- package/server/dist/orchestrator-memory.js.map +1 -0
- package/server/dist/orchestrator-monitor.d.ts +59 -0
- package/server/dist/orchestrator-monitor.js +238 -0
- package/server/dist/orchestrator-monitor.js.map +1 -0
- package/server/dist/orchestrator-reports.d.ts +45 -0
- package/server/dist/orchestrator-reports.js +124 -0
- package/server/dist/orchestrator-reports.js.map +1 -0
- package/server/dist/orchestrator-routes.d.ts +17 -0
- package/server/dist/orchestrator-routes.js +526 -0
- package/server/dist/orchestrator-routes.js.map +1 -0
- package/server/dist/session-archive.js +9 -2
- package/server/dist/session-archive.js.map +1 -1
- package/server/dist/session-manager.d.ts +99 -39
- package/server/dist/session-manager.js +565 -394
- package/server/dist/session-manager.js.map +1 -1
- package/server/dist/session-naming.d.ts +6 -10
- package/server/dist/session-naming.js +60 -62
- package/server/dist/session-naming.js.map +1 -1
- package/server/dist/session-persistence.d.ts +6 -1
- package/server/dist/session-persistence.js +6 -0
- package/server/dist/session-persistence.js.map +1 -1
- package/server/dist/session-restart-scheduler.d.ts +30 -0
- package/server/dist/session-restart-scheduler.js +41 -0
- package/server/dist/session-restart-scheduler.js.map +1 -0
- package/server/dist/session-routes.js +122 -61
- package/server/dist/session-routes.js.map +1 -1
- package/server/dist/stepflow-types.d.ts +1 -1
- package/server/dist/tsconfig.tsbuildinfo +1 -1
- package/server/dist/types.d.ts +34 -2
- package/server/dist/types.js +8 -1
- package/server/dist/types.js.map +1 -1
- package/server/dist/upload-routes.js +7 -1
- package/server/dist/upload-routes.js.map +1 -1
- package/server/dist/version-check.d.ts +17 -0
- package/server/dist/version-check.js +89 -0
- package/server/dist/version-check.js.map +1 -0
- package/server/dist/workflow-engine.d.ts +74 -1
- package/server/dist/workflow-engine.js +20 -1
- package/server/dist/workflow-engine.js.map +1 -1
- package/server/dist/ws-message-handler.js +115 -9
- package/server/dist/ws-message-handler.js.map +1 -1
- package/server/dist/ws-server.js +90 -15
- package/server/dist/ws-server.js.map +1 -1
- package/dist/assets/index-BAdQqYEY.js +0 -182
- package/dist/assets/index-CeZYNLWt.css +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator report reader — scans .codekin/reports/ across managed repos.
|
|
3
|
+
*
|
|
4
|
+
* Discovers audit reports, parses their metadata, and provides them
|
|
5
|
+
* to the orchestrator session for triage and action.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
8
|
+
import { join, basename, resolve } from 'path';
|
|
9
|
+
import { REPOS_ROOT, DATA_DIR } from './config.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Scanner
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const REPORTS_DIR = '.codekin/reports';
|
|
14
|
+
/** Known report categories. */
|
|
15
|
+
const REPORT_CATEGORIES = [
|
|
16
|
+
'code-review',
|
|
17
|
+
'comments',
|
|
18
|
+
'complexity',
|
|
19
|
+
'dependencies',
|
|
20
|
+
'docs-audit',
|
|
21
|
+
'repo-health',
|
|
22
|
+
'security',
|
|
23
|
+
'test-coverage',
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Scan a single repo for all available reports.
|
|
27
|
+
* Returns metadata only (no content) for efficiency.
|
|
28
|
+
*/
|
|
29
|
+
export function scanRepoReports(repoPath) {
|
|
30
|
+
const reportsDir = join(repoPath, REPORTS_DIR);
|
|
31
|
+
if (!existsSync(reportsDir))
|
|
32
|
+
return [];
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const category of REPORT_CATEGORIES) {
|
|
35
|
+
const categoryDir = join(reportsDir, category);
|
|
36
|
+
if (!existsSync(categoryDir))
|
|
37
|
+
continue;
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(categoryDir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const file of entries) {
|
|
46
|
+
if (!file.endsWith('.md'))
|
|
47
|
+
continue;
|
|
48
|
+
const filePath = join(categoryDir, file);
|
|
49
|
+
const stat = statSync(filePath);
|
|
50
|
+
const dateMatch = basename(file).match(/^(\d{4}-\d{2}-\d{2})/);
|
|
51
|
+
const date = dateMatch ? dateMatch[1] : 'unknown';
|
|
52
|
+
results.push({
|
|
53
|
+
filePath,
|
|
54
|
+
category,
|
|
55
|
+
date,
|
|
56
|
+
repoPath,
|
|
57
|
+
size: stat.size,
|
|
58
|
+
mtime: stat.mtime.toISOString(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Sort by date descending (most recent first)
|
|
63
|
+
results.sort((a, b) => b.date.localeCompare(a.date));
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Scan multiple repos for reports.
|
|
68
|
+
*/
|
|
69
|
+
export function scanAllReports(repoPaths) {
|
|
70
|
+
const all = [];
|
|
71
|
+
for (const repoPath of repoPaths) {
|
|
72
|
+
all.push(...scanRepoReports(repoPath));
|
|
73
|
+
}
|
|
74
|
+
all.sort((a, b) => b.date.localeCompare(a.date));
|
|
75
|
+
return all;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read a report's full content.
|
|
79
|
+
*/
|
|
80
|
+
export function readReport(filePath) {
|
|
81
|
+
const resolved = resolve(filePath);
|
|
82
|
+
// Verify the path is within a known reports directory (anchored startsWith, not includes)
|
|
83
|
+
const isDataReport = resolved.startsWith(DATA_DIR + '/reports/');
|
|
84
|
+
const isRepoReport = resolved.startsWith(REPOS_ROOT + '/') &&
|
|
85
|
+
/^[^/]+\/\.codekin\/reports\//.test(resolved.slice(REPOS_ROOT.length + 1));
|
|
86
|
+
if (!isDataReport && !isRepoReport)
|
|
87
|
+
return null;
|
|
88
|
+
if (!existsSync(resolved))
|
|
89
|
+
return null;
|
|
90
|
+
const content = readFileSync(resolved, 'utf-8');
|
|
91
|
+
const stat = statSync(filePath);
|
|
92
|
+
// Extract metadata from path
|
|
93
|
+
const parts = filePath.split('/');
|
|
94
|
+
const reportsIdx = parts.indexOf('reports');
|
|
95
|
+
const category = reportsIdx >= 0 ? parts[reportsIdx + 1] : 'unknown';
|
|
96
|
+
// Find repo path (everything before .codekin/reports)
|
|
97
|
+
const codekinIdx = filePath.indexOf('.codekin/reports');
|
|
98
|
+
const repoPath = codekinIdx >= 0 ? filePath.substring(0, codekinIdx - 1) : '';
|
|
99
|
+
const dateMatch = basename(filePath).match(/^(\d{4}-\d{2}-\d{2})/);
|
|
100
|
+
const date = dateMatch ? dateMatch[1] : 'unknown';
|
|
101
|
+
return {
|
|
102
|
+
filePath,
|
|
103
|
+
category,
|
|
104
|
+
date,
|
|
105
|
+
repoPath,
|
|
106
|
+
size: stat.size,
|
|
107
|
+
mtime: stat.mtime.toISOString(),
|
|
108
|
+
content,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the latest report for a given repo and category.
|
|
113
|
+
*/
|
|
114
|
+
export function getLatestReport(repoPath, category) {
|
|
115
|
+
const reports = scanRepoReports(repoPath);
|
|
116
|
+
return reports.find(r => r.category === category) ?? null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get reports that are newer than a given date.
|
|
120
|
+
*/
|
|
121
|
+
export function getReportsSince(repoPaths, sinceDate) {
|
|
122
|
+
return scanAllReports(repoPaths).filter(r => r.date >= sinceDate);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=orchestrator-reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator-reports.js","sourceRoot":"","sources":["../orchestrator-reports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST routes for the orchestrator session.
|
|
3
|
+
*
|
|
4
|
+
* Provides status, start, report scanning, child session management,
|
|
5
|
+
* memory querying, and trust record endpoints.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import type { Request } from 'express';
|
|
9
|
+
import type { SessionManager } from './session-manager.js';
|
|
10
|
+
import type { OrchestratorMonitor } from './orchestrator-monitor.js';
|
|
11
|
+
type VerifyFn = (token: string | undefined) => boolean;
|
|
12
|
+
type VerifySessionFn = (token: string | undefined, sessionId: string | undefined) => boolean;
|
|
13
|
+
type ExtractFn = (req: Request) => string | undefined;
|
|
14
|
+
export declare function createOrchestratorRouter(verifyToken: VerifyFn, extractToken: ExtractFn, sessions: SessionManager, monitorRef?: {
|
|
15
|
+
current: OrchestratorMonitor | null;
|
|
16
|
+
}, verifyTokenOrSessionToken?: VerifySessionFn): Router;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST routes for the orchestrator session.
|
|
3
|
+
*
|
|
4
|
+
* Provides status, start, report scanning, child session management,
|
|
5
|
+
* memory querying, and trust record endpoints.
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import { existsSync, statSync } from 'fs';
|
|
10
|
+
import { ensureOrchestratorRunning, getOrchestratorSessionId, getOrCreateOrchestratorId } from './orchestrator-manager.js';
|
|
11
|
+
import { getAgentDisplayName, REPOS_ROOT } from './config.js';
|
|
12
|
+
import { scanRepoReports, readReport, getReportsSince } from './orchestrator-reports.js';
|
|
13
|
+
import { OrchestratorMemory } from './orchestrator-memory.js';
|
|
14
|
+
import { OrchestratorChildManager } from './orchestrator-children.js';
|
|
15
|
+
import { extractMemoryCandidates, smartUpsert, runAgingCycle, recordFindingOutcome, getTriageRecommendation, loadSkillProfile, updateSkillLevel, getGuidanceStyle, recordDecision, assessDecisionOutcome, getPendingOutcomeAssessments, } from './orchestrator-learning.js';
|
|
16
|
+
export function createOrchestratorRouter(verifyToken, extractToken, sessions, monitorRef, verifyTokenOrSessionToken) {
|
|
17
|
+
const router = Router();
|
|
18
|
+
const memory = new OrchestratorMemory();
|
|
19
|
+
const children = new OrchestratorChildManager(sessions);
|
|
20
|
+
/**
|
|
21
|
+
* Verify that the request is authorized — accepts either the master auth
|
|
22
|
+
* token OR the orchestrator session's scoped token. This allows the
|
|
23
|
+
* orchestrator's Claude process (which only has a session-scoped token)
|
|
24
|
+
* to call its own management endpoints (spawn children, update memory, etc.).
|
|
25
|
+
*/
|
|
26
|
+
function verifyOrchestratorAuth(req) {
|
|
27
|
+
const token = extractToken(req);
|
|
28
|
+
if (verifyToken(token))
|
|
29
|
+
return true;
|
|
30
|
+
if (verifyTokenOrSessionToken) {
|
|
31
|
+
const orchestratorId = getOrCreateOrchestratorId();
|
|
32
|
+
return verifyTokenOrSessionToken(token, orchestratorId);
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// -------------------------------------------------------------------------
|
|
37
|
+
// Session lifecycle
|
|
38
|
+
// -------------------------------------------------------------------------
|
|
39
|
+
/** Get orchestrator session status. */
|
|
40
|
+
router.get('/api/orchestrator/status', (req, res) => {
|
|
41
|
+
if (!verifyOrchestratorAuth(req))
|
|
42
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
43
|
+
const sessionId = getOrchestratorSessionId(sessions);
|
|
44
|
+
if (!sessionId) {
|
|
45
|
+
return res.json({ sessionId: null, status: 'stopped', agentName: getAgentDisplayName() });
|
|
46
|
+
}
|
|
47
|
+
const session = sessions.get(sessionId);
|
|
48
|
+
const status = session?.claudeProcess?.isAlive() ? 'active' : 'idle';
|
|
49
|
+
res.json({
|
|
50
|
+
sessionId,
|
|
51
|
+
status,
|
|
52
|
+
childSessions: children.activeCount(),
|
|
53
|
+
agentName: getAgentDisplayName(),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
/** Ensure orchestrator is running and return its session ID. */
|
|
57
|
+
router.post('/api/orchestrator/start', (req, res) => {
|
|
58
|
+
if (!verifyOrchestratorAuth(req))
|
|
59
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
60
|
+
try {
|
|
61
|
+
const sessionId = ensureOrchestratorRunning(sessions);
|
|
62
|
+
res.json({ sessionId, status: 'active', agentName: getAgentDisplayName() });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error('[orchestrator] Failed to start:', err);
|
|
66
|
+
res.status(500).json({ error: `Failed to start Agent ${getAgentDisplayName()}` });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
// Reports
|
|
71
|
+
// -------------------------------------------------------------------------
|
|
72
|
+
/** Scan reports for a single repo. */
|
|
73
|
+
router.get('/api/orchestrator/reports', (req, res) => {
|
|
74
|
+
if (!verifyOrchestratorAuth(req))
|
|
75
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
76
|
+
const repoPath = req.query.repo;
|
|
77
|
+
const since = req.query.since;
|
|
78
|
+
if (repoPath) {
|
|
79
|
+
const reports = scanRepoReports(repoPath);
|
|
80
|
+
res.json({ reports });
|
|
81
|
+
}
|
|
82
|
+
else if (since) {
|
|
83
|
+
// Scan all managed repos — get paths from memory
|
|
84
|
+
const repoItems = memory.list({ memoryType: 'repo_context' });
|
|
85
|
+
const repoPaths = repoItems.map(r => r.scope).filter((s) => !!s);
|
|
86
|
+
const reports = getReportsSince(repoPaths, since);
|
|
87
|
+
res.json({ reports });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
res.status(400).json({ error: 'Provide ?repo=<path> or ?since=<YYYY-MM-DD>' });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
/** Read a specific report's content. */
|
|
94
|
+
router.get('/api/orchestrator/reports/read', (req, res) => {
|
|
95
|
+
if (!verifyOrchestratorAuth(req))
|
|
96
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
97
|
+
const filePath = req.query.path;
|
|
98
|
+
if (!filePath)
|
|
99
|
+
return res.status(400).json({ error: 'Provide ?path=<filePath>' });
|
|
100
|
+
const report = readReport(filePath);
|
|
101
|
+
if (!report)
|
|
102
|
+
return res.status(404).json({ error: 'Report not found' });
|
|
103
|
+
res.json({ report });
|
|
104
|
+
});
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// Child sessions
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
/** List child sessions. */
|
|
109
|
+
router.get('/api/orchestrator/children', (req, res) => {
|
|
110
|
+
if (!verifyOrchestratorAuth(req))
|
|
111
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
112
|
+
res.json({ children: children.list() });
|
|
113
|
+
});
|
|
114
|
+
/** Spawn a child session. */
|
|
115
|
+
router.post('/api/orchestrator/children', async (req, res) => {
|
|
116
|
+
if (!verifyOrchestratorAuth(req))
|
|
117
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
118
|
+
const { repo, task, branchName, completionPolicy, deployAfter, useWorktree, model } = req.body;
|
|
119
|
+
if (!repo || !task || !branchName) {
|
|
120
|
+
return res.status(400).json({ error: 'Missing required fields: repo, task, branchName' });
|
|
121
|
+
}
|
|
122
|
+
// Validate branchName to prevent prompt injection
|
|
123
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$/.test(branchName)) {
|
|
124
|
+
return res.status(400).json({ error: 'Invalid branchName: only alphanumeric, /, _, ., and - are allowed' });
|
|
125
|
+
}
|
|
126
|
+
// Validate repo path: must resolve under REPOS_ROOT and be an existing directory
|
|
127
|
+
const resolvedRepo = resolve(repo);
|
|
128
|
+
if (!resolvedRepo.startsWith(REPOS_ROOT + '/') && resolvedRepo !== REPOS_ROOT) {
|
|
129
|
+
return res.status(400).json({ error: 'Invalid repo path: must be under configured repos root' });
|
|
130
|
+
}
|
|
131
|
+
if (!existsSync(resolvedRepo) || !statSync(resolvedRepo).isDirectory()) {
|
|
132
|
+
return res.status(400).json({ error: 'Invalid repo path: directory does not exist' });
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const child = await children.spawn({
|
|
136
|
+
repo,
|
|
137
|
+
task,
|
|
138
|
+
branchName,
|
|
139
|
+
completionPolicy: completionPolicy ?? 'pr',
|
|
140
|
+
deployAfter: deployAfter ?? false,
|
|
141
|
+
useWorktree: useWorktree ?? true,
|
|
142
|
+
model,
|
|
143
|
+
});
|
|
144
|
+
res.json({ child });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
res.status(503).json({ error: err instanceof Error ? err.message : 'Failed to spawn child session' });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
/** Get a specific child session. */
|
|
151
|
+
router.get('/api/orchestrator/children/:id', (req, res) => {
|
|
152
|
+
if (!verifyOrchestratorAuth(req))
|
|
153
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
154
|
+
const child = children.get(req.params.id);
|
|
155
|
+
if (!child)
|
|
156
|
+
return res.status(404).json({ error: 'Child session not found' });
|
|
157
|
+
res.json({ child });
|
|
158
|
+
});
|
|
159
|
+
// -------------------------------------------------------------------------
|
|
160
|
+
// Memory
|
|
161
|
+
// -------------------------------------------------------------------------
|
|
162
|
+
/** Search memory. */
|
|
163
|
+
router.get('/api/orchestrator/memory', (req, res) => {
|
|
164
|
+
if (!verifyOrchestratorAuth(req))
|
|
165
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
166
|
+
const query = req.query.q;
|
|
167
|
+
const type = req.query.type;
|
|
168
|
+
const limit = parseInt(req.query.limit || '20', 10);
|
|
169
|
+
if (query) {
|
|
170
|
+
const items = memory.search(query, limit);
|
|
171
|
+
res.json({ items });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const items = memory.list({
|
|
175
|
+
memoryType: type,
|
|
176
|
+
limit,
|
|
177
|
+
});
|
|
178
|
+
res.json({ items });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
/** Add or update a memory item. */
|
|
182
|
+
router.post('/api/orchestrator/memory', (req, res) => {
|
|
183
|
+
if (!verifyOrchestratorAuth(req))
|
|
184
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
185
|
+
const { id, memoryType, scope, title, content, sourceRef, confidence, expiresAt, isPinned, tags } = req.body;
|
|
186
|
+
if (!memoryType || !content) {
|
|
187
|
+
return res.status(400).json({ error: 'Missing required fields: memoryType, content' });
|
|
188
|
+
}
|
|
189
|
+
const itemId = memory.upsert({
|
|
190
|
+
id,
|
|
191
|
+
memoryType,
|
|
192
|
+
scope: scope ?? null,
|
|
193
|
+
title: title ?? null,
|
|
194
|
+
content,
|
|
195
|
+
sourceRef: sourceRef ?? null,
|
|
196
|
+
confidence: confidence ?? 0.8,
|
|
197
|
+
expiresAt: expiresAt ?? null,
|
|
198
|
+
isPinned: isPinned ?? false,
|
|
199
|
+
tags: tags ?? [],
|
|
200
|
+
});
|
|
201
|
+
res.json({ id: itemId });
|
|
202
|
+
});
|
|
203
|
+
/** Delete a memory item. */
|
|
204
|
+
router.delete('/api/orchestrator/memory/:id', (req, res) => {
|
|
205
|
+
if (!verifyOrchestratorAuth(req))
|
|
206
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
207
|
+
const deleted = memory.delete(req.params.id);
|
|
208
|
+
res.json({ deleted });
|
|
209
|
+
});
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// Trust
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
/** List all trust records. */
|
|
214
|
+
router.get('/api/orchestrator/trust', (req, res) => {
|
|
215
|
+
if (!verifyOrchestratorAuth(req))
|
|
216
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
217
|
+
res.json({ records: memory.listTrustRecords() });
|
|
218
|
+
});
|
|
219
|
+
/** Compute trust level for an action. */
|
|
220
|
+
router.get('/api/orchestrator/trust/level', (req, res) => {
|
|
221
|
+
if (!verifyOrchestratorAuth(req))
|
|
222
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
223
|
+
const { action, category, severity, repo } = req.query;
|
|
224
|
+
if (!action || !category) {
|
|
225
|
+
return res.status(400).json({ error: 'Provide ?action=X&category=Y' });
|
|
226
|
+
}
|
|
227
|
+
const level = memory.computeTrustLevel(action, category, severity ?? 'medium', repo ?? null);
|
|
228
|
+
res.json({ level });
|
|
229
|
+
});
|
|
230
|
+
/** Record an approval. */
|
|
231
|
+
router.post('/api/orchestrator/trust/approve', (req, res) => {
|
|
232
|
+
if (!verifyOrchestratorAuth(req))
|
|
233
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
234
|
+
const { action, category, repo } = req.body;
|
|
235
|
+
if (!action || !category) {
|
|
236
|
+
return res.status(400).json({ error: 'Missing required fields: action, category' });
|
|
237
|
+
}
|
|
238
|
+
const record = memory.recordApproval(action, category, repo ?? null);
|
|
239
|
+
res.json({ record });
|
|
240
|
+
});
|
|
241
|
+
/** Record a rejection (resets trust to ASK). */
|
|
242
|
+
router.post('/api/orchestrator/trust/reject', (req, res) => {
|
|
243
|
+
if (!verifyOrchestratorAuth(req))
|
|
244
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
245
|
+
const { action, category, repo } = req.body;
|
|
246
|
+
if (!action || !category) {
|
|
247
|
+
return res.status(400).json({ error: 'Missing required fields: action, category' });
|
|
248
|
+
}
|
|
249
|
+
const record = memory.recordRejection(action, category, repo ?? null);
|
|
250
|
+
res.json({ record });
|
|
251
|
+
});
|
|
252
|
+
/** Pin trust to a specific level (user override). */
|
|
253
|
+
router.post('/api/orchestrator/trust/pin', (req, res) => {
|
|
254
|
+
if (!verifyOrchestratorAuth(req))
|
|
255
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
256
|
+
const { action, category, repo, level } = req.body;
|
|
257
|
+
if (!action || !category || !level) {
|
|
258
|
+
return res.status(400).json({ error: 'Missing required fields: action, category, level' });
|
|
259
|
+
}
|
|
260
|
+
memory.pinTrust(action, category, repo ?? null, level);
|
|
261
|
+
res.json({ ok: true });
|
|
262
|
+
});
|
|
263
|
+
/** Reset all trust records. */
|
|
264
|
+
router.post('/api/orchestrator/trust/reset', (req, res) => {
|
|
265
|
+
if (!verifyOrchestratorAuth(req))
|
|
266
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
267
|
+
memory.resetAllTrust();
|
|
268
|
+
res.json({ ok: true });
|
|
269
|
+
});
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
// Notifications
|
|
272
|
+
// -------------------------------------------------------------------------
|
|
273
|
+
/** Get pending notifications from the monitor. */
|
|
274
|
+
router.get('/api/orchestrator/notifications', (req, res) => {
|
|
275
|
+
if (!verifyOrchestratorAuth(req))
|
|
276
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
277
|
+
const monitor = monitorRef?.current;
|
|
278
|
+
if (!monitor)
|
|
279
|
+
return res.json({ notifications: [] });
|
|
280
|
+
const all = req.query.all === 'true';
|
|
281
|
+
res.json({ notifications: all ? monitor.getAll() : monitor.getPending() });
|
|
282
|
+
});
|
|
283
|
+
/** Mark notifications as delivered. */
|
|
284
|
+
router.post('/api/orchestrator/notifications/mark-delivered', (req, res) => {
|
|
285
|
+
if (!verifyOrchestratorAuth(req))
|
|
286
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
287
|
+
const monitor = monitorRef?.current;
|
|
288
|
+
if (!monitor)
|
|
289
|
+
return res.json({ ok: true });
|
|
290
|
+
const { ids } = req.body;
|
|
291
|
+
if (Array.isArray(ids))
|
|
292
|
+
monitor.markDelivered(ids);
|
|
293
|
+
res.json({ ok: true });
|
|
294
|
+
});
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
// Dashboard stats
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
/** Get summary stats for the dashboard header. */
|
|
299
|
+
router.get('/api/orchestrator/dashboard', (req, res) => {
|
|
300
|
+
if (!verifyOrchestratorAuth(req))
|
|
301
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
302
|
+
const repoItems = memory.list({ memoryType: 'repo_context' });
|
|
303
|
+
const pendingNotifications = monitorRef?.current?.getPending() ?? [];
|
|
304
|
+
const activeChildren = children.activeCount();
|
|
305
|
+
const trustRecords = memory.listTrustRecords();
|
|
306
|
+
const autoApproved = trustRecords.filter(t => t.effectiveLevel !== 'ask').length;
|
|
307
|
+
res.json({
|
|
308
|
+
stats: {
|
|
309
|
+
managedRepos: repoItems.length,
|
|
310
|
+
pendingNotifications: pendingNotifications.length,
|
|
311
|
+
activeChildSessions: activeChildren,
|
|
312
|
+
totalChildSessions: children.list().length,
|
|
313
|
+
trustRecords: trustRecords.length,
|
|
314
|
+
autoApprovedActions: autoApproved,
|
|
315
|
+
memoryItems: memory.list().length,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
// -------------------------------------------------------------------------
|
|
320
|
+
// Memory extraction & learning (Phase 4)
|
|
321
|
+
// -------------------------------------------------------------------------
|
|
322
|
+
/** Extract memory candidates from a session interaction. */
|
|
323
|
+
router.post('/api/orchestrator/memory/extract', (req, res) => {
|
|
324
|
+
if (!verifyOrchestratorAuth(req))
|
|
325
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
326
|
+
const { userMessage, assistantResponse, repo, sourceRef } = req.body;
|
|
327
|
+
if (!userMessage || !assistantResponse) {
|
|
328
|
+
return res.status(400).json({ error: 'Missing required fields: userMessage, assistantResponse' });
|
|
329
|
+
}
|
|
330
|
+
const candidates = extractMemoryCandidates(userMessage, assistantResponse, repo ?? null);
|
|
331
|
+
const results = candidates.map(c => smartUpsert(memory, c, sourceRef ?? null));
|
|
332
|
+
res.json({ candidates: candidates.length, results });
|
|
333
|
+
});
|
|
334
|
+
/** Run the aging/decay cycle. */
|
|
335
|
+
router.post('/api/orchestrator/memory/age', (req, res) => {
|
|
336
|
+
if (!verifyOrchestratorAuth(req))
|
|
337
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
338
|
+
const result = runAgingCycle(memory);
|
|
339
|
+
res.json(result);
|
|
340
|
+
});
|
|
341
|
+
// -------------------------------------------------------------------------
|
|
342
|
+
// Finding outcomes & triage recommendations
|
|
343
|
+
// -------------------------------------------------------------------------
|
|
344
|
+
/** Record a finding outcome. */
|
|
345
|
+
router.post('/api/orchestrator/findings/outcome', (req, res) => {
|
|
346
|
+
if (!verifyOrchestratorAuth(req))
|
|
347
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
348
|
+
const { findingId, repo, category, severity, action, reason, sessionId, outcome } = req.body;
|
|
349
|
+
if (!findingId || !repo || !category || !action) {
|
|
350
|
+
return res.status(400).json({ error: 'Missing required fields' });
|
|
351
|
+
}
|
|
352
|
+
const id = recordFindingOutcome(memory, {
|
|
353
|
+
findingId, repo, category,
|
|
354
|
+
severity: severity ?? 'medium',
|
|
355
|
+
action, reason: reason ?? '',
|
|
356
|
+
sessionId: sessionId ?? null,
|
|
357
|
+
outcome: outcome ?? null,
|
|
358
|
+
timestamp: new Date().toISOString(),
|
|
359
|
+
});
|
|
360
|
+
res.json({ id });
|
|
361
|
+
});
|
|
362
|
+
/** Get triage recommendation based on historical patterns. */
|
|
363
|
+
router.get('/api/orchestrator/findings/recommend', (req, res) => {
|
|
364
|
+
if (!verifyOrchestratorAuth(req))
|
|
365
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
366
|
+
const { category, severity, repo } = req.query;
|
|
367
|
+
if (!category)
|
|
368
|
+
return res.status(400).json({ error: 'Provide ?category=X' });
|
|
369
|
+
const recommendation = getTriageRecommendation(memory, category, severity ?? 'medium', repo ?? null);
|
|
370
|
+
res.json(recommendation);
|
|
371
|
+
});
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// User skill model
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
/** Get the user's skill profile. */
|
|
376
|
+
router.get('/api/orchestrator/skills', (req, res) => {
|
|
377
|
+
if (!verifyOrchestratorAuth(req))
|
|
378
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
379
|
+
res.json({
|
|
380
|
+
profile: loadSkillProfile(),
|
|
381
|
+
guidanceStyle: getGuidanceStyle(),
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
/** Update a skill level based on an observed signal. */
|
|
385
|
+
router.post('/api/orchestrator/skills', (req, res) => {
|
|
386
|
+
if (!verifyOrchestratorAuth(req))
|
|
387
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
388
|
+
const { domain, signal, level } = req.body;
|
|
389
|
+
if (!domain || !signal || !level) {
|
|
390
|
+
return res.status(400).json({ error: 'Missing required fields: domain, signal, level' });
|
|
391
|
+
}
|
|
392
|
+
const updated = updateSkillLevel(domain, signal, level);
|
|
393
|
+
res.json({ skill: updated, guidanceStyle: getGuidanceStyle() });
|
|
394
|
+
});
|
|
395
|
+
// -------------------------------------------------------------------------
|
|
396
|
+
// Decision history
|
|
397
|
+
// -------------------------------------------------------------------------
|
|
398
|
+
/** Record a decision. */
|
|
399
|
+
router.post('/api/orchestrator/decisions', (req, res) => {
|
|
400
|
+
if (!verifyOrchestratorAuth(req))
|
|
401
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
402
|
+
const { decision, rationale, repo, relatedFinding, expectedOutcome } = req.body;
|
|
403
|
+
if (!decision || !rationale) {
|
|
404
|
+
return res.status(400).json({ error: 'Missing required fields: decision, rationale' });
|
|
405
|
+
}
|
|
406
|
+
const id = recordDecision(memory, {
|
|
407
|
+
decision, rationale,
|
|
408
|
+
repo: repo ?? null,
|
|
409
|
+
relatedFinding: relatedFinding ?? null,
|
|
410
|
+
expectedOutcome: expectedOutcome ?? '',
|
|
411
|
+
});
|
|
412
|
+
res.json({ id });
|
|
413
|
+
});
|
|
414
|
+
/** Assess a decision's outcome. */
|
|
415
|
+
router.post('/api/orchestrator/decisions/:id/assess', (req, res) => {
|
|
416
|
+
if (!verifyOrchestratorAuth(req))
|
|
417
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
418
|
+
const { actualOutcome } = req.body;
|
|
419
|
+
if (!actualOutcome)
|
|
420
|
+
return res.status(400).json({ error: 'Missing required field: actualOutcome' });
|
|
421
|
+
const updated = assessDecisionOutcome(memory, req.params.id, actualOutcome);
|
|
422
|
+
res.json({ updated });
|
|
423
|
+
});
|
|
424
|
+
/** Get decisions pending outcome assessment. */
|
|
425
|
+
router.get('/api/orchestrator/decisions/pending', (req, res) => {
|
|
426
|
+
if (!verifyOrchestratorAuth(req))
|
|
427
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
428
|
+
res.json({ decisions: getPendingOutcomeAssessments(memory) });
|
|
429
|
+
});
|
|
430
|
+
// -------------------------------------------------------------------------
|
|
431
|
+
// Session prompts & approvals
|
|
432
|
+
// -------------------------------------------------------------------------
|
|
433
|
+
/** Get all sessions with pending prompts (waiting for approval or answer). */
|
|
434
|
+
router.get('/api/orchestrator/sessions/pending-prompts', (req, res) => {
|
|
435
|
+
if (!verifyOrchestratorAuth(req))
|
|
436
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
437
|
+
res.json({ sessions: sessions.getPendingPrompts() });
|
|
438
|
+
});
|
|
439
|
+
/** Approve or deny a pending prompt in any session. */
|
|
440
|
+
router.post('/api/orchestrator/sessions/:id/respond', (req, res) => {
|
|
441
|
+
if (!verifyOrchestratorAuth(req))
|
|
442
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
443
|
+
const sessionId = req.params.id;
|
|
444
|
+
const { requestId, value } = req.body;
|
|
445
|
+
if (!value) {
|
|
446
|
+
return res.status(400).json({ error: 'Missing required field: value (e.g. "allow", "deny", or answer text)' });
|
|
447
|
+
}
|
|
448
|
+
const session = sessions.get(sessionId);
|
|
449
|
+
if (!session)
|
|
450
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
451
|
+
// Verify there's actually a pending prompt (optionally for the specific requestId)
|
|
452
|
+
const hasPending = requestId
|
|
453
|
+
? (session.pendingToolApprovals.has(requestId) || session.pendingControlRequests.has(requestId))
|
|
454
|
+
: (session.pendingToolApprovals.size > 0 || session.pendingControlRequests.size > 0);
|
|
455
|
+
if (!hasPending) {
|
|
456
|
+
return res.status(409).json({ error: 'No pending prompt to respond to' });
|
|
457
|
+
}
|
|
458
|
+
// Capture prompt details before responding (response clears them)
|
|
459
|
+
let promptToolName = 'unknown';
|
|
460
|
+
let promptType = 'permission';
|
|
461
|
+
if (requestId) {
|
|
462
|
+
const toolApproval = session.pendingToolApprovals.get(requestId);
|
|
463
|
+
const controlReq = session.pendingControlRequests.get(requestId);
|
|
464
|
+
if (toolApproval) {
|
|
465
|
+
promptToolName = toolApproval.toolName;
|
|
466
|
+
promptType = toolApproval.toolName === 'AskUserQuestion' ? 'question' : 'permission';
|
|
467
|
+
}
|
|
468
|
+
else if (controlReq) {
|
|
469
|
+
promptToolName = controlReq.toolName;
|
|
470
|
+
promptType = controlReq.toolName === 'AskUserQuestion' ? 'question' : 'permission';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
sessions.sendPromptResponse(sessionId, value, requestId);
|
|
474
|
+
// Broadcast a notification to the orchestrator channel so users can see
|
|
475
|
+
// what the orchestrator approved/denied/answered.
|
|
476
|
+
const orchestratorId = getOrCreateOrchestratorId();
|
|
477
|
+
const orchestratorSession = sessions.get(orchestratorId);
|
|
478
|
+
if (orchestratorSession && orchestratorSession.clients.size > 0) {
|
|
479
|
+
const actionLabel = promptType === 'question'
|
|
480
|
+
? `answered question from ${promptToolName}`
|
|
481
|
+
: `responded "${value}" to ${promptToolName}`;
|
|
482
|
+
const notifMsg = {
|
|
483
|
+
type: 'system_message',
|
|
484
|
+
subtype: 'info',
|
|
485
|
+
text: `[${getAgentDisplayName()}] ${actionLabel} in session "${session.name}"`,
|
|
486
|
+
};
|
|
487
|
+
for (const ws of orchestratorSession.clients) {
|
|
488
|
+
ws.send(JSON.stringify(notifMsg));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
res.json({ ok: true });
|
|
492
|
+
});
|
|
493
|
+
// -------------------------------------------------------------------------
|
|
494
|
+
// Session cleanup
|
|
495
|
+
// -------------------------------------------------------------------------
|
|
496
|
+
/** List all sessions (unfiltered, includes source field). */
|
|
497
|
+
router.get('/api/orchestrator/sessions', (req, res) => {
|
|
498
|
+
if (!verifyOrchestratorAuth(req))
|
|
499
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
500
|
+
res.json({ sessions: sessions.listAll() });
|
|
501
|
+
});
|
|
502
|
+
/** Delete all automated sessions (source: workflow, webhook, stepflow, agent). */
|
|
503
|
+
router.delete('/api/orchestrator/sessions/cleanup', (req, res) => {
|
|
504
|
+
if (!verifyOrchestratorAuth(req))
|
|
505
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
506
|
+
const automatedSources = new Set(['workflow', 'webhook', 'stepflow', 'agent']);
|
|
507
|
+
const toDelete = sessions.listAll().filter((s) => automatedSources.has(s.source ?? ''));
|
|
508
|
+
let deleted = 0;
|
|
509
|
+
for (const s of toDelete) {
|
|
510
|
+
if (sessions.delete(s.id))
|
|
511
|
+
deleted++;
|
|
512
|
+
}
|
|
513
|
+
res.json({ deleted });
|
|
514
|
+
});
|
|
515
|
+
/** Delete a specific session by ID. */
|
|
516
|
+
router.delete('/api/orchestrator/sessions/:id', (req, res) => {
|
|
517
|
+
if (!verifyOrchestratorAuth(req))
|
|
518
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
519
|
+
const success = sessions.delete(req.params.id);
|
|
520
|
+
if (!success)
|
|
521
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
522
|
+
res.json({ deleted: true });
|
|
523
|
+
});
|
|
524
|
+
return router;
|
|
525
|
+
}
|
|
526
|
+
//# sourceMappingURL=orchestrator-routes.js.map
|