dsh-harbor-evolution 0.3.1 → 0.4.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 +13 -5
- package/index.js +10 -6
- package/lib/client.js +395 -0
- package/lib/dashboard.js +143 -0
- package/lib/service.js +30 -0
- package/lib/setup.js +7 -1
- package/lib/web.js +63 -0
- package/package.json +22 -4
package/lib/dashboard.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { access, constants, readdir, readFile, stat } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { resolveWithin } from './evolution.js'
|
|
5
|
+
|
|
6
|
+
const SUMMARY_NAME = 'evaluation-summary.json'
|
|
7
|
+
const PROMOTION_NAME = 'promotion-report.json'
|
|
8
|
+
const MAX_JOBS = 50
|
|
9
|
+
|
|
10
|
+
async function readJson(file) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(await readFile(file, 'utf8'))
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT') return undefined
|
|
15
|
+
if (error instanceof SyntaxError) return { __readError: `invalid JSON in ${path.basename(file)}` }
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function directoryCheck(directory, { optional = false } = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const details = await stat(directory)
|
|
23
|
+
if (!details.isDirectory()) return { status: 'error', detail: 'not a directory' }
|
|
24
|
+
await access(directory, constants.R_OK)
|
|
25
|
+
return { status: 'ok', detail: 'readable' }
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (optional && error.code === 'ENOENT') return { status: 'warning', detail: 'not created yet' }
|
|
28
|
+
return { status: 'error', detail: error.code === 'ENOENT' ? 'not found' : error.message }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function executableCheck(command) {
|
|
33
|
+
if (!command) return { status: 'error', detail: 'not configured' }
|
|
34
|
+
if (!path.isAbsolute(command)) return { status: 'ok', detail: `${command} (resolved from PATH)` }
|
|
35
|
+
try {
|
|
36
|
+
await access(command, constants.X_OK)
|
|
37
|
+
return { status: 'ok', detail: command }
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return { status: 'error', detail: error.code === 'ENOENT' ? `${command} not found` : error.message }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function jobStatus(summary) {
|
|
44
|
+
if (!summary) return 'pending'
|
|
45
|
+
if (summary.__readError) return 'failed'
|
|
46
|
+
const trials = Number(summary.n_trials ?? 0)
|
|
47
|
+
const exceptions = Number(summary.n_exceptions ?? 0)
|
|
48
|
+
if (trials > 0 && exceptions >= trials) return 'failed'
|
|
49
|
+
if (exceptions > 0) return 'partial'
|
|
50
|
+
return 'completed'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readJob(jobsDir, entry, details) {
|
|
54
|
+
const directory = path.join(jobsDir, entry.name)
|
|
55
|
+
const summary = await readJson(path.join(directory, SUMMARY_NAME))
|
|
56
|
+
const promotion = await readJson(path.join(directory, PROMOTION_NAME))
|
|
57
|
+
return {
|
|
58
|
+
name: entry.name,
|
|
59
|
+
path: directory,
|
|
60
|
+
updatedAt: details.mtime.toISOString(),
|
|
61
|
+
status: jobStatus(summary),
|
|
62
|
+
nTrials: Number(summary?.n_trials ?? 0),
|
|
63
|
+
nExceptions: Number(summary?.n_exceptions ?? 0),
|
|
64
|
+
metrics: summary?.metrics ?? {},
|
|
65
|
+
candidate: summary?.candidate ?? undefined,
|
|
66
|
+
evaluationContext: summary?.evaluation_context ?? undefined,
|
|
67
|
+
promotion: promotion ? {
|
|
68
|
+
decision: promotion.decision,
|
|
69
|
+
reasons: Array.isArray(promotion.reasons) ? promotion.reasons : [],
|
|
70
|
+
baselineJob: promotion.baseline_job,
|
|
71
|
+
candidateJob: promotion.candidate_job,
|
|
72
|
+
} : undefined,
|
|
73
|
+
readError: summary?.__readError,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function listJobs(jobsDir) {
|
|
78
|
+
let entries
|
|
79
|
+
try {
|
|
80
|
+
entries = await readdir(jobsDir, { withFileTypes: true })
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === 'ENOENT') return []
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
const recent = await Promise.all(entries
|
|
86
|
+
.filter(entry => entry.isDirectory())
|
|
87
|
+
.map(async entry => ({ entry, details: await stat(path.join(jobsDir, entry.name)) })))
|
|
88
|
+
recent.sort((left, right) => right.details.mtimeMs - left.details.mtimeMs)
|
|
89
|
+
return Promise.all(recent
|
|
90
|
+
.slice(0, MAX_JOBS)
|
|
91
|
+
.map(({ entry, details }) => readJob(jobsDir, entry, details)))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function latestMetric(jobs) {
|
|
95
|
+
const completed = jobs.find(job => job.status === 'completed' || job.status === 'partial')
|
|
96
|
+
if (!completed) return undefined
|
|
97
|
+
const entry = Object.entries(completed.metrics).find(([, value]) => typeof value === 'number')
|
|
98
|
+
return entry ? { name: entry[0], value: entry[1] } : undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function readDashboardSnapshot(config, metadata = {}) {
|
|
102
|
+
const projectRoot = path.resolve(config.projectRoot)
|
|
103
|
+
const jobsDir = resolveWithin(projectRoot, config.jobsDir, 'jobsDir')
|
|
104
|
+
const [jobs, projectRootCheck, jobsDirCheck, harborCheck, harborDshCheck] = await Promise.all([
|
|
105
|
+
listJobs(jobsDir),
|
|
106
|
+
directoryCheck(projectRoot),
|
|
107
|
+
directoryCheck(jobsDir, { optional: true }),
|
|
108
|
+
executableCheck(config.harborBin),
|
|
109
|
+
executableCheck(config.harborDshBin),
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
const counts = jobs.reduce((result, job) => {
|
|
113
|
+
result[job.status] = (result[job.status] ?? 0) + 1
|
|
114
|
+
return result
|
|
115
|
+
}, {})
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
generatedAt: new Date().toISOString(),
|
|
120
|
+
pluginVersion: metadata.pluginVersion ?? 'development',
|
|
121
|
+
config: {
|
|
122
|
+
projectRoot,
|
|
123
|
+
jobsDir,
|
|
124
|
+
dshVersion: config.dshVersion,
|
|
125
|
+
agentImportPath: config.agentImportPath,
|
|
126
|
+
pluginImportPath: config.pluginImportPath,
|
|
127
|
+
},
|
|
128
|
+
checks: {
|
|
129
|
+
projectRoot: projectRootCheck,
|
|
130
|
+
jobsDir: jobsDirCheck,
|
|
131
|
+
harbor: harborCheck,
|
|
132
|
+
harborDsh: harborDshCheck,
|
|
133
|
+
},
|
|
134
|
+
overview: {
|
|
135
|
+
totalJobs: jobs.length,
|
|
136
|
+
completedJobs: (counts.completed ?? 0) + (counts.partial ?? 0),
|
|
137
|
+
activeJobs: counts.pending ?? 0,
|
|
138
|
+
failedJobs: counts.failed ?? 0,
|
|
139
|
+
latestMetric: latestMetric(jobs),
|
|
140
|
+
},
|
|
141
|
+
jobs,
|
|
142
|
+
}
|
|
143
|
+
}
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readDashboardSnapshot } from './dashboard.js'
|
|
2
|
+
import { compareCandidates, readEvaluation, runEvaluation, snapshot } from './evolution.js'
|
|
3
|
+
|
|
4
|
+
/** One Host-side boundary shared by Agent tools and the Web dashboard. */
|
|
5
|
+
export class EvolutionService {
|
|
6
|
+
constructor(config, metadata = {}) {
|
|
7
|
+
this.config = config
|
|
8
|
+
this.metadata = metadata
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
snapshot(args) {
|
|
12
|
+
return snapshot(this.config, args)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
run(args) {
|
|
16
|
+
return runEvaluation(this.config, args)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
result(args) {
|
|
20
|
+
return readEvaluation(this.config, args)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
compare(args) {
|
|
24
|
+
return compareCandidates(this.config, args)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dashboard() {
|
|
28
|
+
return readDashboardSnapshot(this.config, this.metadata)
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/setup.js
CHANGED
|
@@ -247,6 +247,10 @@ export async function setupIntegration(raw = {}, dependencies = {}) {
|
|
|
247
247
|
// the complete locked graph there so runtime dependencies and host peers
|
|
248
248
|
// do not disappear behind the profile's `link:` entry.
|
|
249
249
|
await run('npm', ['ci', '--ignore-scripts'], { cwd: localPluginDir })
|
|
250
|
+
// The browser half is generated from source and embeds its visual asset.
|
|
251
|
+
// Build explicitly because the locked install above intentionally skips
|
|
252
|
+
// lifecycle scripts for deterministic source-checkout setup.
|
|
253
|
+
await run('npm', ['run', 'build'], { cwd: localPluginDir })
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
progress('2/4 Installing the Harbor Python runtime...')
|
|
@@ -310,7 +314,9 @@ export function renderSetupResult(result) {
|
|
|
310
314
|
`cd ${shellQuote(result.projectRoot)}`,
|
|
311
315
|
startCommand,
|
|
312
316
|
'',
|
|
313
|
-
|
|
317
|
+
result.profile === 'web'
|
|
318
|
+
? 'Open the Harbor tab, or invoke: /evolve-agent-with-harbor'
|
|
319
|
+
: 'Then invoke: /evolve-agent-with-harbor',
|
|
314
320
|
)
|
|
315
321
|
return lines.join('\n')
|
|
316
322
|
}
|
package/lib/web.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const DASHBOARD_ROUTE = '/_dsh/harbor-evolution/dashboard'
|
|
2
|
+
|
|
3
|
+
function sendJson(response, status, body) {
|
|
4
|
+
response.writeHead(status, {
|
|
5
|
+
'cache-control': 'no-store',
|
|
6
|
+
'content-type': 'application/json; charset=utf-8',
|
|
7
|
+
'x-content-type-options': 'nosniff',
|
|
8
|
+
})
|
|
9
|
+
response.end(JSON.stringify(body))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isSameOriginRequest(request) {
|
|
13
|
+
const fetchSite = request.headers['sec-fetch-site']
|
|
14
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') return false
|
|
15
|
+
const origin = request.headers.origin
|
|
16
|
+
if (!origin) {
|
|
17
|
+
if (fetchSite === 'same-origin' || fetchSite === 'none') return true
|
|
18
|
+
const address = request.socket?.remoteAddress ?? ''
|
|
19
|
+
return address === '::1' || address === '127.0.0.1' || address.startsWith('127.')
|
|
20
|
+
|| address.startsWith('::ffff:127.')
|
|
21
|
+
}
|
|
22
|
+
const host = request.headers.host
|
|
23
|
+
if (!host) return false
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(origin)
|
|
26
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createDashboardHandler(service) {
|
|
33
|
+
return (request, response) => {
|
|
34
|
+
if (request.method !== 'GET') {
|
|
35
|
+
response.writeHead(405, { allow: 'GET' })
|
|
36
|
+
response.end()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
if (!isSameOriginRequest(request)) {
|
|
40
|
+
sendJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin request required' } })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
Promise.resolve(service.dashboard()).then(
|
|
44
|
+
value => sendJson(response, 200, { ok: true, value }),
|
|
45
|
+
error => sendJson(response, 500, {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: { code: 'dashboard-unavailable', message: error instanceof Error ? error.message : String(error) },
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Add the dashboard route only in profiles that provide the optional Web service. */
|
|
54
|
+
export function installDashboardWeb(ctx, service) {
|
|
55
|
+
if (typeof ctx.inject !== 'function') return
|
|
56
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
57
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
58
|
+
kind: 'exact',
|
|
59
|
+
path: DASHBOARD_ROUTE,
|
|
60
|
+
handler: createDashboardHandler(service),
|
|
61
|
+
}), 'harbor-evolution: dashboard route')
|
|
62
|
+
})
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-harbor-evolution",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DeepSeek Harness plugin and bundled Skill for safely evolving Cordis Candidates with Harbor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
|
-
"exports":
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
8
12
|
"bin": {
|
|
9
13
|
"dsh-harbor": "bin/dsh-harbor.mjs"
|
|
10
14
|
},
|
|
@@ -18,12 +22,24 @@
|
|
|
18
22
|
"LICENSE"
|
|
19
23
|
],
|
|
20
24
|
"scripts": {
|
|
25
|
+
"build": "node scripts/build-client.mjs",
|
|
26
|
+
"prepack": "npm run build",
|
|
21
27
|
"test": "node --test",
|
|
22
|
-
"check": "node --check index.js && node --check bin/dsh-harbor.mjs && node --test"
|
|
28
|
+
"check": "npm run build && node --check index.js && node --check bin/dsh-harbor.mjs && node --test"
|
|
23
29
|
},
|
|
24
30
|
"dsh": {
|
|
25
31
|
"bundle": {
|
|
26
32
|
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"inject": [
|
|
36
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
37
|
+
"@deepseek-ai/dsh-client-locale",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-tool",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
27
43
|
}
|
|
28
44
|
},
|
|
29
45
|
"peerDependencies": {
|
|
@@ -37,7 +53,9 @@
|
|
|
37
53
|
"@deepseek-ai/cordis": "4.0.1",
|
|
38
54
|
"@deepseek-ai/dsh-skill": "0.1.0-rc.6",
|
|
39
55
|
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
40
|
-
"@deepseek-ai/schemastery": "3.18.1"
|
|
56
|
+
"@deepseek-ai/schemastery": "3.18.1",
|
|
57
|
+
"esbuild": "0.28.2",
|
|
58
|
+
"react": "18.3.1"
|
|
41
59
|
},
|
|
42
60
|
"engines": {
|
|
43
61
|
"node": ">=22"
|