dsh-harbor-evolution 0.1.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/LICENSE +21 -0
- package/README.md +47 -0
- package/bin/dsh-harbor.mjs +39 -0
- package/cordis.patch.yml +3 -0
- package/index.js +97 -0
- package/lib/candidate.js +88 -0
- package/lib/evolution.js +88 -0
- package/lib/process.js +31 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 istarwyh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# dsh-harbor-evolution
|
|
2
|
+
|
|
3
|
+
Cordis bundle for evaluating immutable DeepSeek Harness Candidates with Harbor.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install `harbor-dsh-evolution` into the same Python environment as Harbor, then add this bundle to the DSH profile:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv venv .venv
|
|
11
|
+
uv pip install --python .venv/bin/python harbor-dsh-evolution
|
|
12
|
+
source .venv/bin/activate
|
|
13
|
+
pnpm dlx @deepseek-ai/dsh@0.1.0-rc.6 plugin --profile headless add dsh-harbor-evolution
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For development from this repository:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
./hse dsh-install headless
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`-w` is required by pnpm when the DSH profile directory is its workspace root.
|
|
23
|
+
|
|
24
|
+
The bundle inserts one `harbor-evolution` entry and registers four model-facing tools:
|
|
25
|
+
|
|
26
|
+
- `harbor_candidate_snapshot`
|
|
27
|
+
- `harbor_eval_run`
|
|
28
|
+
- `harbor_eval_result`
|
|
29
|
+
- `harbor_candidate_compare`
|
|
30
|
+
|
|
31
|
+
The shortest evaluation call needs only `candidatePath` and `datasetPath`. Candidate identity defaults to `package.json`, the Job name is generated automatically, and `harbor_eval_run` returns the completed evaluation summary directly.
|
|
32
|
+
|
|
33
|
+
The helper launches a fixed DSH version through `pnpm dlx`, so it does not conflict with a long-running `npx` DSH process. When installed from this monorepo, the bundle automatically discovers the sibling Python virtual environment created by `./hse dsh-install`. Otherwise it uses `HARBOR_BIN` / `HARBOR_DSH_BIN`, then falls back to `PATH`. Override the inserted entry in the profile's `cordis.patch.yml` when the binaries or Python source live elsewhere:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
- id: harbor-evolution
|
|
37
|
+
config:
|
|
38
|
+
projectRoot: /workspace/my-agent
|
|
39
|
+
jobsDir: jobs
|
|
40
|
+
harborBin: /workspace/venv/bin/harbor
|
|
41
|
+
harborDshBin: /workspace/venv/bin/harbor-dsh
|
|
42
|
+
pythonPath: /workspace/harbor-self-evolving/packages/harbor-plugin/src
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This replaces the complete config for the entry, following DSH patch semantics.
|
|
46
|
+
|
|
47
|
+
The plugin never mutates the active DSH profile and never deploys a Candidate. Another system updates the Champion only after the external Promotion Gate passes.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { snapshotCandidate } from '../lib/candidate.js'
|
|
5
|
+
import { runProcess } from '../lib/process.js'
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
console.error('Usage: dsh-harbor snapshot <candidate-dir> [--id <id>] [--version <version>]')
|
|
9
|
+
console.error(' dsh-harbor doctor')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const [command, ...args] = process.argv.slice(2)
|
|
14
|
+
if (command === 'doctor') {
|
|
15
|
+
const harbor = await runProcess(process.env.HARBOR_BIN || 'harbor', ['--version'], { timeoutMs: 10000 })
|
|
16
|
+
const plugins = await runProcess(process.env.HARBOR_BIN || 'harbor', ['plugins', 'list'], { timeoutMs: 10000 })
|
|
17
|
+
console.log(JSON.stringify({ harbor: harbor.stdout.trim(), plugins: plugins.stdout.trim() }, null, 2))
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (command === 'snapshot') {
|
|
21
|
+
const candidateDir = args.shift()
|
|
22
|
+
const idIndex = args.indexOf('--id')
|
|
23
|
+
const versionIndex = args.indexOf('--version')
|
|
24
|
+
if (!candidateDir) throw new Error('snapshot requires candidate-dir')
|
|
25
|
+
const manifest = await snapshotCandidate(candidateDir, {
|
|
26
|
+
candidateId: idIndex < 0 ? undefined : args[idIndex + 1],
|
|
27
|
+
version: versionIndex < 0 ? undefined : args[versionIndex + 1],
|
|
28
|
+
})
|
|
29
|
+
console.log(JSON.stringify(manifest, null, 2))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
usage()
|
|
33
|
+
process.exitCode = 2
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main().catch(error => {
|
|
37
|
+
console.error(error.message)
|
|
38
|
+
process.exitCode = 1
|
|
39
|
+
})
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { compareCandidates, readEvaluation, runEvaluation, snapshot } from './lib/evolution.js'
|
|
8
|
+
|
|
9
|
+
export const name = 'harbor-evolution'
|
|
10
|
+
export const inject = ['tools']
|
|
11
|
+
|
|
12
|
+
const packageDir = path.dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const checkoutPythonPackage = path.resolve(packageDir, '../harbor-plugin')
|
|
14
|
+
|
|
15
|
+
function checkoutExecutable(name) {
|
|
16
|
+
const candidate = path.join(checkoutPythonPackage, '.venv', 'bin', name)
|
|
17
|
+
return existsSync(candidate) ? candidate : name
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Config = Schema.object({
|
|
21
|
+
projectRoot: Schema.string().default('.'),
|
|
22
|
+
jobsDir: Schema.string().default('jobs'),
|
|
23
|
+
harborBin: Schema.string().default(''),
|
|
24
|
+
harborDshBin: Schema.string().default(''),
|
|
25
|
+
dshVersion: Schema.string().default('0.1.0-rc.6'),
|
|
26
|
+
agentImportPath: Schema.string().default('harbor_dsh_evolution.agent:DshCandidateAgent'),
|
|
27
|
+
pluginImportPath: Schema.string().default('dsh-evolution'),
|
|
28
|
+
pythonPath: Schema.string().default(''),
|
|
29
|
+
timeoutMs: Schema.number().default(1800000),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function jsonTool(definition, execute) {
|
|
33
|
+
return defineTool({
|
|
34
|
+
...definition,
|
|
35
|
+
output: {
|
|
36
|
+
schema: { type: 'string' },
|
|
37
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
38
|
+
},
|
|
39
|
+
async execute(args) {
|
|
40
|
+
return JSON.stringify(await execute(args), null, 2)
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function apply(ctx, config) {
|
|
46
|
+
const resolved = {
|
|
47
|
+
...config,
|
|
48
|
+
projectRoot: path.resolve(config.projectRoot),
|
|
49
|
+
harborBin: config.harborBin || process.env.HARBOR_BIN || checkoutExecutable('harbor'),
|
|
50
|
+
harborDshBin: config.harborDshBin || process.env.HARBOR_DSH_BIN || checkoutExecutable('harbor-dsh'),
|
|
51
|
+
pythonPath: config.pythonPath || (
|
|
52
|
+
existsSync(path.join(checkoutPythonPackage, 'src'))
|
|
53
|
+
? path.join(checkoutPythonPackage, 'src')
|
|
54
|
+
: ''
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ctx.tools.register(jsonTool({
|
|
59
|
+
name: 'harbor_candidate_snapshot',
|
|
60
|
+
description: 'Freeze a DeepSeek Harness Cordis composition as an immutable Candidate manifest. Candidate id and version default to package.json.',
|
|
61
|
+
parameters: {
|
|
62
|
+
candidatePath: { type: 'string', required: true },
|
|
63
|
+
candidateId: { type: 'string' },
|
|
64
|
+
version: { type: 'string' },
|
|
65
|
+
},
|
|
66
|
+
}, args => snapshot(resolved, args)))
|
|
67
|
+
|
|
68
|
+
ctx.tools.register(jsonTool({
|
|
69
|
+
name: 'harbor_eval_run',
|
|
70
|
+
description: 'Snapshot and run one DeepSeek Harness Candidate against a Harbor dataset, then return the completed evaluation summary.',
|
|
71
|
+
parameters: {
|
|
72
|
+
candidatePath: { type: 'string', required: true },
|
|
73
|
+
candidateId: { type: 'string' },
|
|
74
|
+
version: { type: 'string' },
|
|
75
|
+
datasetPath: { type: 'string', required: true },
|
|
76
|
+
jobName: { type: 'string' },
|
|
77
|
+
},
|
|
78
|
+
}, args => runEvaluation(resolved, args)))
|
|
79
|
+
|
|
80
|
+
ctx.tools.register(jsonTool({
|
|
81
|
+
name: 'harbor_eval_result',
|
|
82
|
+
description: 'Read the stable evaluation summary produced for a Harbor Job.',
|
|
83
|
+
parameters: {
|
|
84
|
+
jobPath: { type: 'string', required: true },
|
|
85
|
+
},
|
|
86
|
+
}, args => readEvaluation(resolved, args)))
|
|
87
|
+
|
|
88
|
+
ctx.tools.register(jsonTool({
|
|
89
|
+
name: 'harbor_candidate_compare',
|
|
90
|
+
description: 'Apply the deterministic Promotion Gate to a baseline Job and a Candidate Job.',
|
|
91
|
+
parameters: {
|
|
92
|
+
baselineJob: { type: 'string', required: true },
|
|
93
|
+
candidateJob: { type: 'string', required: true },
|
|
94
|
+
policyPath: { type: 'string', required: true },
|
|
95
|
+
},
|
|
96
|
+
}, args => compareCandidates(resolved, args)))
|
|
97
|
+
}
|
package/lib/candidate.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const MANIFEST_NAME = 'candidate-manifest.json'
|
|
6
|
+
const DIGEST_PREFIX = Buffer.from('harbor-dsh-candidate-v1\0')
|
|
7
|
+
const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__'])
|
|
8
|
+
const EXCLUDED_FILES = new Set([MANIFEST_NAME, '.DS_Store'])
|
|
9
|
+
|
|
10
|
+
async function walk(root, current = root) {
|
|
11
|
+
const entries = await readdir(current, { withFileTypes: true })
|
|
12
|
+
const files = []
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
const absolute = path.join(current, entry.name)
|
|
15
|
+
const relative = path.relative(root, absolute).split(path.sep).join('/')
|
|
16
|
+
if (entry.isSymbolicLink()) throw new Error(`Candidate must not contain symlinks: ${relative}`)
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
if (!EXCLUDED_DIRS.has(entry.name)) files.push(...await walk(root, absolute))
|
|
19
|
+
} else if (entry.isFile() && !EXCLUDED_FILES.has(entry.name)) {
|
|
20
|
+
files.push({ absolute, relative })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return files.sort((a, b) => Buffer.compare(Buffer.from(a.relative), Buffer.from(b.relative)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function computeCandidate(candidateDir) {
|
|
27
|
+
const root = path.resolve(candidateDir)
|
|
28
|
+
if (!(await stat(root)).isDirectory()) throw new Error(`Candidate path is not a directory: ${root}`)
|
|
29
|
+
const digest = createHash('sha256')
|
|
30
|
+
digest.update(DIGEST_PREFIX)
|
|
31
|
+
const files = []
|
|
32
|
+
for (const item of await walk(root)) {
|
|
33
|
+
const content = await readFile(item.absolute)
|
|
34
|
+
files.push({
|
|
35
|
+
path: item.relative,
|
|
36
|
+
size: content.length,
|
|
37
|
+
sha256: createHash('sha256').update(content).digest('hex'),
|
|
38
|
+
})
|
|
39
|
+
digest.update(item.relative)
|
|
40
|
+
digest.update('\0')
|
|
41
|
+
digest.update(String(content.length))
|
|
42
|
+
digest.update('\0')
|
|
43
|
+
digest.update(content)
|
|
44
|
+
digest.update('\0')
|
|
45
|
+
}
|
|
46
|
+
return { digest: `sha256:${digest.digest('hex')}`, files }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function snapshotCandidate(candidateDir, options = {}) {
|
|
50
|
+
const root = path.resolve(candidateDir)
|
|
51
|
+
for (const required of ['cordis.yml', 'package.json']) {
|
|
52
|
+
try {
|
|
53
|
+
if (!(await stat(path.join(root, required))).isFile()) throw new Error()
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error(`Candidate is missing required file: ${required}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let packageJson
|
|
59
|
+
try {
|
|
60
|
+
packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'))
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`Candidate package.json is not valid JSON: ${error.message}`)
|
|
63
|
+
}
|
|
64
|
+
const candidateId = options.candidateId ?? packageJson.name
|
|
65
|
+
const version = options.version ?? packageJson.version
|
|
66
|
+
const runtimeVersion = options.runtimeVersion ?? '0.1.0-rc.6'
|
|
67
|
+
if (!candidateId || !version || !runtimeVersion) {
|
|
68
|
+
throw new Error('Candidate id, version, and runtime version must not be empty; set package.json name/version or pass explicit values')
|
|
69
|
+
}
|
|
70
|
+
const computed = await computeCandidate(root)
|
|
71
|
+
const manifest = {
|
|
72
|
+
schema_version: 1,
|
|
73
|
+
candidate_id: String(candidateId),
|
|
74
|
+
version: String(version),
|
|
75
|
+
digest: computed.digest,
|
|
76
|
+
created_at: new Date().toISOString(),
|
|
77
|
+
runtime: {
|
|
78
|
+
kind: 'deepseek-harness',
|
|
79
|
+
version: String(runtimeVersion),
|
|
80
|
+
transport: 'acp',
|
|
81
|
+
},
|
|
82
|
+
files: computed.files,
|
|
83
|
+
metadata: options.metadata ?? {},
|
|
84
|
+
}
|
|
85
|
+
await mkdir(root, { recursive: true })
|
|
86
|
+
await writeFile(path.join(root, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
87
|
+
return manifest
|
|
88
|
+
}
|
package/lib/evolution.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { MANIFEST_NAME, snapshotCandidate } from './candidate.js'
|
|
5
|
+
import { runProcess } from './process.js'
|
|
6
|
+
|
|
7
|
+
export function resolveWithin(root, value, label) {
|
|
8
|
+
const base = path.resolve(root)
|
|
9
|
+
const resolved = path.resolve(base, value)
|
|
10
|
+
if (resolved !== base && !resolved.startsWith(`${base}${path.sep}`)) {
|
|
11
|
+
throw new Error(`${label} must stay under projectRoot`)
|
|
12
|
+
}
|
|
13
|
+
return resolved
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function snapshot(config, args) {
|
|
17
|
+
const candidateDir = resolveWithin(config.projectRoot, args.candidatePath, 'candidatePath')
|
|
18
|
+
return snapshotCandidate(candidateDir, {
|
|
19
|
+
candidateId: args.candidateId,
|
|
20
|
+
version: args.version,
|
|
21
|
+
runtimeVersion: config.dshVersion,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function slug(value) {
|
|
26
|
+
return String(value)
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
29
|
+
.replace(/^[._-]+|[._-]+$/g, '') || 'candidate'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function makeJobName(manifest, now = new Date()) {
|
|
33
|
+
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '')
|
|
34
|
+
const suffix = `${timestamp}-${manifest.digest.slice(7, 15)}`
|
|
35
|
+
const available = 100 - suffix.length - 1
|
|
36
|
+
const identity = `${slug(manifest.candidate_id)}-${slug(manifest.version)}`.slice(0, available)
|
|
37
|
+
return `${identity}-${suffix}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runEvaluation(config, args) {
|
|
41
|
+
const manifest = await snapshot(config, args)
|
|
42
|
+
const candidateDir = resolveWithin(config.projectRoot, args.candidatePath, 'candidatePath')
|
|
43
|
+
const dataset = resolveWithin(config.projectRoot, args.datasetPath, 'datasetPath')
|
|
44
|
+
const jobsDir = resolveWithin(config.projectRoot, config.jobsDir, 'jobsDir')
|
|
45
|
+
const jobName = args.jobName ?? makeJobName(manifest)
|
|
46
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(jobName)) throw new Error('jobName contains unsupported characters')
|
|
47
|
+
|
|
48
|
+
const harborArgs = [
|
|
49
|
+
'run', '-p', dataset,
|
|
50
|
+
'-a', config.agentImportPath,
|
|
51
|
+
'--ak', `candidate_path=${candidateDir}`,
|
|
52
|
+
'--ak', `candidate_version=${manifest.version}`,
|
|
53
|
+
'--ak', `candidate_digest=${manifest.digest}`,
|
|
54
|
+
'--job-name', jobName,
|
|
55
|
+
'--jobs-dir', jobsDir,
|
|
56
|
+
'--plugin', config.pluginImportPath,
|
|
57
|
+
'--plugin-kwarg', `candidate_manifest=${path.join(candidateDir, MANIFEST_NAME)}`,
|
|
58
|
+
]
|
|
59
|
+
const result = await runProcess(config.harborBin, harborArgs, {
|
|
60
|
+
cwd: config.projectRoot,
|
|
61
|
+
timeoutMs: config.timeoutMs,
|
|
62
|
+
env: { ...process.env, ...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}) },
|
|
63
|
+
})
|
|
64
|
+
const jobDir = path.join(jobsDir, jobName)
|
|
65
|
+
const summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
|
|
66
|
+
return { manifest, jobDir, summary, process: { code: result.code } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function readEvaluation(config, args) {
|
|
70
|
+
const jobDir = resolveWithin(config.projectRoot, args.jobPath, 'jobPath')
|
|
71
|
+
return JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function compareCandidates(config, args) {
|
|
75
|
+
const baseline = resolveWithin(config.projectRoot, args.baselineJob, 'baselineJob')
|
|
76
|
+
const candidate = resolveWithin(config.projectRoot, args.candidateJob, 'candidateJob')
|
|
77
|
+
const policy = resolveWithin(config.projectRoot, args.policyPath, 'policyPath')
|
|
78
|
+
const result = await runProcess(config.harborDshBin, [
|
|
79
|
+
'promote', baseline, candidate, '--policy', policy,
|
|
80
|
+
], {
|
|
81
|
+
cwd: config.projectRoot,
|
|
82
|
+
timeoutMs: config.timeoutMs,
|
|
83
|
+
// harbor-dsh uses exit 1 for a valid REJECT decision. The report remains
|
|
84
|
+
// model-readable; infrastructure failures still reject the subprocess.
|
|
85
|
+
allowedExitCodes: [0, 1],
|
|
86
|
+
})
|
|
87
|
+
return JSON.parse(result.stdout)
|
|
88
|
+
}
|
package/lib/process.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export function runProcess(command, args, options = {}) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
cwd: options.cwd,
|
|
7
|
+
env: options.env ?? process.env,
|
|
8
|
+
shell: false,
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
+
})
|
|
11
|
+
let stdout = ''
|
|
12
|
+
let stderr = ''
|
|
13
|
+
const timeout = setTimeout(() => {
|
|
14
|
+
child.kill('SIGTERM')
|
|
15
|
+
reject(new Error(`Command timed out after ${options.timeoutMs}ms: ${command}`))
|
|
16
|
+
}, options.timeoutMs ?? 1_800_000)
|
|
17
|
+
child.stdout.on('data', chunk => { stdout += chunk })
|
|
18
|
+
child.stderr.on('data', chunk => { stderr += chunk })
|
|
19
|
+
child.on('error', error => {
|
|
20
|
+
clearTimeout(timeout)
|
|
21
|
+
reject(error)
|
|
22
|
+
})
|
|
23
|
+
child.on('close', code => {
|
|
24
|
+
clearTimeout(timeout)
|
|
25
|
+
const result = { command, args, code, stdout, stderr }
|
|
26
|
+
const allowedExitCodes = options.allowedExitCodes ?? [0]
|
|
27
|
+
if (allowedExitCodes.includes(code)) resolve(result)
|
|
28
|
+
else reject(Object.assign(new Error(`Command failed with exit code ${code}: ${command}`), { result }))
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-harbor-evolution",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin for snapshotting Cordis Candidates and evaluating them with Harbor.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": "./index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"dsh-harbor": "bin/dsh-harbor.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"lib/",
|
|
14
|
+
"bin/",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"check": "node --check index.js && node --check bin/dsh-harbor.mjs && node --test"
|
|
22
|
+
},
|
|
23
|
+
"dsh": {
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
36
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"keywords": [
|
|
43
|
+
"deepseek-harness",
|
|
44
|
+
"dsh-plugin",
|
|
45
|
+
"harbor",
|
|
46
|
+
"agent-evaluation",
|
|
47
|
+
"agent-evolution",
|
|
48
|
+
"cordis"
|
|
49
|
+
],
|
|
50
|
+
"homepage": "https://github.com/istarwyh/harbor-self-evolving#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/istarwyh/harbor-self-evolving/issues"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/istarwyh/harbor-self-evolving.git",
|
|
57
|
+
"directory": "packages/dsh-plugin"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|