dsh-harbor-evolution 0.7.2 → 0.7.3
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 +9 -4
- package/index.js +32 -6
- package/lib/candidate.js +58 -5
- package/lib/client.js +195 -54
- package/lib/dashboard.js +141 -20
- package/lib/evolution.js +139 -20
- package/lib/model-runtime.js +46 -5
- package/lib/runtime-identity.js +7 -0
- package/lib/service.js +187 -33
- package/lib/setup.js +2 -2
- package/lib/version.js +128 -0
- package/lib/web.js +5 -1
- package/package.json +8 -3
- package/skills/evolve-agent-with-harbor/SKILL.md +81 -7
- package/skills/evolve-agent-with-harbor/evals/evals.json +48 -3
- package/skills/evolve-agent-with-harbor/references/evaluator-upgrade.md +31 -0
- package/skills/evolve-agent-with-harbor/references/initialization.md +9 -2
package/lib/service.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { access } from 'node:fs/promises'
|
|
1
|
+
import { access, stat } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
+
import { loadModelBinding } from './candidate.js'
|
|
5
|
+
|
|
4
6
|
import {
|
|
7
|
+
discoverWorkspaceConfigs,
|
|
5
8
|
readComparison,
|
|
6
9
|
readDashboardSnapshot,
|
|
7
10
|
readDatasetPreview,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
compareCandidates,
|
|
17
20
|
initializeGroundTruth,
|
|
18
21
|
initializeProject,
|
|
22
|
+
initializeQuickDiagnostic,
|
|
19
23
|
inspectEvaluator,
|
|
20
24
|
previewContext,
|
|
21
25
|
readEvaluation,
|
|
@@ -25,7 +29,9 @@ import {
|
|
|
25
29
|
snapshot,
|
|
26
30
|
updateEvaluator,
|
|
27
31
|
validateDataset,
|
|
32
|
+
resolveWithin,
|
|
28
33
|
} from './evolution.js'
|
|
34
|
+
import { createVersionChecker } from './version.js'
|
|
29
35
|
|
|
30
36
|
export async function resolveEvaluatorStackPath(config, governance, explicitPath) {
|
|
31
37
|
if (explicitPath) return explicitPath
|
|
@@ -49,9 +55,54 @@ export async function resolveEvaluatorStackPath(config, governance, explicitPath
|
|
|
49
55
|
/** One Host-side boundary shared by Agent tools and the Web dashboard. */
|
|
50
56
|
export class EvolutionService {
|
|
51
57
|
constructor(config, metadata = {}, modelRuntime) {
|
|
52
|
-
this.config = config
|
|
58
|
+
this.config = { jobsDir: 'jobs', ...config }
|
|
53
59
|
this.metadata = metadata
|
|
54
60
|
this.modelRuntime = modelRuntime
|
|
61
|
+
this.versionChecker = metadata.versionChecker ?? createVersionChecker()
|
|
62
|
+
this.projectRoots = new Map()
|
|
63
|
+
this.workspaceConfigs = new Map()
|
|
64
|
+
this.activeProjectRoot = path.resolve(this.config.projectRoot)
|
|
65
|
+
this._registerProjectRoot(this.activeProjectRoot, metadata.projectRootSource ?? 'configured')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_registerProjectRoot(projectRoot, source) {
|
|
69
|
+
const resolved = path.resolve(projectRoot)
|
|
70
|
+
this.projectRoots.set(resolved, { projectRoot: resolved, source, activatedAt: new Date().toISOString() })
|
|
71
|
+
this.activeProjectRoot = resolved
|
|
72
|
+
return this.projectRoots.get(resolved)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async _refreshWorkspaces() {
|
|
76
|
+
const discovered = []
|
|
77
|
+
this.workspaceConfigs.clear()
|
|
78
|
+
for (const [identity, root] of this.projectRoots.entries()) {
|
|
79
|
+
try {
|
|
80
|
+
const details = await stat(root.projectRoot)
|
|
81
|
+
if (!details.isDirectory()) throw new Error('not a directory')
|
|
82
|
+
} catch {
|
|
83
|
+
this.projectRoots.delete(identity)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const configs = await discoverWorkspaceConfigs({ ...this.config, projectRoot: root.projectRoot })
|
|
87
|
+
for (const config of configs) {
|
|
88
|
+
const value = { ...config, projectRootSource: root.source }
|
|
89
|
+
this.workspaceConfigs.set(config.workspaceId, value)
|
|
90
|
+
discovered.push(value)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return discovered
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async _webContext(args = {}) {
|
|
97
|
+
const workspaces = await this._refreshWorkspaces()
|
|
98
|
+
const requested = String(args.workspace ?? '').trim()
|
|
99
|
+
let config = requested ? this.workspaceConfigs.get(requested) : undefined
|
|
100
|
+
if (requested && !config) throw new Error('Workspace is unavailable; reload Harbor and select an active workspace')
|
|
101
|
+
config ??= workspaces.find(item => item.projectRoot === this.activeProjectRoot && item.workspaceRoot === '.')
|
|
102
|
+
?? workspaces.find(item => item.projectRoot === this.activeProjectRoot)
|
|
103
|
+
?? workspaces[0]
|
|
104
|
+
if (!config) throw new Error('No Harbor workspace is available')
|
|
105
|
+
return { config, workspaces }
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
snapshot(args) {
|
|
@@ -62,8 +113,22 @@ export class EvolutionService {
|
|
|
62
113
|
return initializeProject(this.config, args)
|
|
63
114
|
}
|
|
64
115
|
|
|
116
|
+
quickDiagnostic(args) {
|
|
117
|
+
return initializeQuickDiagnostic(this.config, args)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _resolveCandidateModel(args) {
|
|
121
|
+
const candidatePath = resolveWithin(
|
|
122
|
+
this.config.projectRoot,
|
|
123
|
+
args.candidatePath,
|
|
124
|
+
'candidatePath',
|
|
125
|
+
)
|
|
126
|
+
const pinnedBinding = await loadModelBinding(candidatePath)
|
|
127
|
+
return this.modelRuntime.resolve(args, pinnedBinding)
|
|
128
|
+
}
|
|
129
|
+
|
|
65
130
|
async run(args) {
|
|
66
|
-
const candidateModelBinding = await this.
|
|
131
|
+
const candidateModelBinding = await this._resolveCandidateModel(args)
|
|
67
132
|
return runEvaluation(this.config, { ...args, candidateModelBinding }, this.modelRuntime)
|
|
68
133
|
}
|
|
69
134
|
|
|
@@ -85,7 +150,7 @@ export class EvolutionService {
|
|
|
85
150
|
}
|
|
86
151
|
|
|
87
152
|
async doctor(args) {
|
|
88
|
-
const candidateModelBinding = await this.
|
|
153
|
+
const candidateModelBinding = await this._resolveCandidateModel(args)
|
|
89
154
|
const result = await runDoctor(this.config, args)
|
|
90
155
|
return { ...result, candidate_model_binding: candidateModelBinding }
|
|
91
156
|
}
|
|
@@ -95,54 +160,141 @@ export class EvolutionService {
|
|
|
95
160
|
}
|
|
96
161
|
|
|
97
162
|
async previewContext(args) {
|
|
98
|
-
const candidateModelBinding = await this.
|
|
163
|
+
const candidateModelBinding = await this._resolveCandidateModel(args)
|
|
99
164
|
return previewContext(this.config, { ...args, candidateModelBinding })
|
|
100
165
|
}
|
|
101
166
|
|
|
102
|
-
dashboard() {
|
|
103
|
-
|
|
167
|
+
async dashboard(args = {}) {
|
|
168
|
+
const { config, workspaces } = await this._webContext(args)
|
|
169
|
+
return readDashboardSnapshot(config, {
|
|
170
|
+
...this.metadata,
|
|
171
|
+
projectRootSource: config.projectRootSource,
|
|
172
|
+
workspaces: workspaces.map(item => ({
|
|
173
|
+
id: item.workspaceId,
|
|
174
|
+
label: item.workspaceLabel,
|
|
175
|
+
root: item.workspaceRoot,
|
|
176
|
+
projectRoot: item.projectRoot,
|
|
177
|
+
jobsDir: item.jobsDir,
|
|
178
|
+
stackPath: item.stackPath,
|
|
179
|
+
source: item.projectRootSource,
|
|
180
|
+
})),
|
|
181
|
+
}, args)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async version(args = {}) {
|
|
185
|
+
let config = this.config
|
|
186
|
+
if (args.workspace) ({ config } = await this._webContext(args))
|
|
187
|
+
else {
|
|
188
|
+
try { ({ config } = await this._webContext(args)) } catch {}
|
|
189
|
+
}
|
|
190
|
+
return this.versionChecker({
|
|
191
|
+
currentVersion: this.metadata.pluginVersion ?? 'development',
|
|
192
|
+
projectRoot: config.projectRoot,
|
|
193
|
+
refresh: args.refresh === true || args.refresh === 'true',
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async modelBinding() {
|
|
198
|
+
const binding = await this.modelRuntime.currentBinding()
|
|
199
|
+
return {
|
|
200
|
+
schema_version: 1,
|
|
201
|
+
scope: 'new-candidate',
|
|
202
|
+
candidate_model_binding: binding,
|
|
203
|
+
transport: 'dsh-host-broker',
|
|
204
|
+
protocol: 'dsh-host-model-gateway/v1',
|
|
205
|
+
credentials: {
|
|
206
|
+
mode: 'host-broker-only',
|
|
207
|
+
note: 'The Candidate receives only a short-lived Job capability. Host OAuth and API credentials never enter the Candidate or Harbor artifacts.',
|
|
208
|
+
},
|
|
209
|
+
note: 'Write candidate_model_binding to model-binding.json before snapshotting. Later chat-model changes do not rewrite this Candidate.',
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
activateProjectRoot(requested, source = 'agent-session') {
|
|
214
|
+
if (!path.isAbsolute(requested)) throw new Error('projectRoot must be an absolute directory path')
|
|
215
|
+
const resolved = path.resolve(requested)
|
|
216
|
+
this._registerProjectRoot(resolved, source)
|
|
217
|
+
return {
|
|
218
|
+
projectRoot: resolved,
|
|
219
|
+
reloaded: true,
|
|
220
|
+
source,
|
|
221
|
+
scope: 'Web Workbench only; Agent tools remain isolated to each calling session working directory.',
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async setProjectRoot(args) {
|
|
226
|
+
const requested = String(args?.projectRoot ?? '').trim()
|
|
227
|
+
if (!path.isAbsolute(requested)) throw new Error('projectRoot must be an absolute directory path')
|
|
228
|
+
const resolved = path.resolve(requested)
|
|
229
|
+
const details = await stat(resolved)
|
|
230
|
+
if (!details.isDirectory()) throw new Error('projectRoot must point to an existing directory')
|
|
231
|
+
return this.activateProjectRoot(resolved, 'manual')
|
|
104
232
|
}
|
|
105
233
|
|
|
106
|
-
job(args) {
|
|
107
|
-
|
|
234
|
+
async job(args) {
|
|
235
|
+
const { config } = await this._webContext(args)
|
|
236
|
+
return readJobDetail(config, args)
|
|
108
237
|
}
|
|
109
238
|
|
|
110
|
-
trials(args) {
|
|
111
|
-
|
|
239
|
+
async trials(args) {
|
|
240
|
+
const { config } = await this._webContext(args)
|
|
241
|
+
return readTrialsPage(config, args)
|
|
112
242
|
}
|
|
113
243
|
|
|
114
|
-
trial(args) {
|
|
115
|
-
|
|
244
|
+
async trial(args) {
|
|
245
|
+
const { config } = await this._webContext(args)
|
|
246
|
+
return readTrialDetail(config, args)
|
|
116
247
|
}
|
|
117
248
|
|
|
118
|
-
dataset(args) {
|
|
119
|
-
|
|
249
|
+
async dataset(args) {
|
|
250
|
+
const { config } = await this._webContext(args)
|
|
251
|
+
return readDatasetPreview(config, args)
|
|
120
252
|
}
|
|
121
253
|
|
|
122
|
-
progress(args) {
|
|
123
|
-
|
|
254
|
+
async progress(args) {
|
|
255
|
+
const { config } = await this._webContext(args)
|
|
256
|
+
return readJobProgress(config, args)
|
|
124
257
|
}
|
|
125
258
|
|
|
126
|
-
comparison(args) {
|
|
127
|
-
|
|
259
|
+
async comparison(args) {
|
|
260
|
+
const { config } = await this._webContext(args)
|
|
261
|
+
return readComparison(config, args)
|
|
128
262
|
}
|
|
129
263
|
|
|
130
264
|
async governance(args) {
|
|
131
|
-
const
|
|
265
|
+
const { config } = await this._webContext(args)
|
|
266
|
+
const governance = await readEvaluatorGovernance(config, args)
|
|
132
267
|
try {
|
|
133
|
-
const stackPath = await resolveEvaluatorStackPath(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
268
|
+
const stackPath = await resolveEvaluatorStackPath(config, governance, args.stackPath)
|
|
269
|
+
const current = await inspectEvaluator(config, { ...args, stackPath })
|
|
270
|
+
const historicalEvaluator = governance.components?.evaluator
|
|
271
|
+
const identityMatches = current.stack?.id === governance.stackIdentity.id
|
|
272
|
+
&& current.stack?.version === governance.stackIdentity.version
|
|
273
|
+
&& current.evaluator?.evaluator_id === historicalEvaluator?.id
|
|
274
|
+
&& current.evaluator?.version === historicalEvaluator?.version
|
|
275
|
+
&& current.evaluator?.digest === historicalEvaluator?.digest
|
|
276
|
+
if (!identityMatches) {
|
|
277
|
+
governance.evaluatorInterface = {
|
|
278
|
+
error: 'The live Evaluator no longer matches this historical Job. Historical sources remain readable, but editing is disabled until you open a Job with the current Stack identity.',
|
|
279
|
+
}
|
|
280
|
+
governance.editingPolicy.identityMatch = false
|
|
281
|
+
} else {
|
|
282
|
+
governance.evaluatorInterface = current
|
|
283
|
+
governance.editingPolicy.browserWriteEnabled = true
|
|
284
|
+
governance.editingPolicy.identityMatch = true
|
|
285
|
+
governance.editingPolicy.stackPath = current.stack?.path
|
|
286
|
+
governance.editingPolicy.saveBehavior = 'Update one descriptor-authorized file with optimistic concurrency and create new Evaluator and Stack identities.'
|
|
287
|
+
}
|
|
138
288
|
} catch (error) {
|
|
139
289
|
governance.evaluatorInterface = { error: error instanceof Error ? error.message : String(error) }
|
|
290
|
+
governance.editingPolicy.identityMatch = false
|
|
140
291
|
}
|
|
141
292
|
return governance
|
|
142
293
|
}
|
|
143
294
|
|
|
144
|
-
evaluator(args) {
|
|
145
|
-
|
|
295
|
+
async evaluator(args) {
|
|
296
|
+
const config = args.workspace ? (await this._webContext(args)).config : this.config
|
|
297
|
+
return updateEvaluator(config, args)
|
|
146
298
|
}
|
|
147
299
|
|
|
148
300
|
evaluatorInspect(args) {
|
|
@@ -158,13 +310,15 @@ export class EvolutionService {
|
|
|
158
310
|
}
|
|
159
311
|
|
|
160
312
|
async meta(args) {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
313
|
+
const { config } = await this._webContext(args)
|
|
314
|
+
const governance = await readEvaluatorGovernance(config, args)
|
|
315
|
+
const stackPath = await resolveEvaluatorStackPath(config, governance, args.stackPath)
|
|
316
|
+
if (!stackPath) return readMetaEvaluation(config, args)
|
|
317
|
+
const stackDirectory = path.dirname(path.resolve(config.projectRoot, stackPath))
|
|
165
318
|
const evaluationRoot = path.dirname(stackDirectory)
|
|
166
|
-
return readMetaEvaluation(
|
|
167
|
-
|
|
319
|
+
return readMetaEvaluation(config, {
|
|
320
|
+
...args,
|
|
321
|
+
evaluationRoot: path.relative(config.projectRoot, evaluationRoot),
|
|
168
322
|
})
|
|
169
323
|
}
|
|
170
324
|
}
|
package/lib/setup.js
CHANGED
|
@@ -5,14 +5,14 @@ import process from 'node:process'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
import { runProcess } from './process.js'
|
|
8
|
-
|
|
9
|
-
export const DSH_VERSION = '0.1.0-rc.6'
|
|
8
|
+
import { DSH_RUNTIME_VERSION } from './runtime-identity.js'
|
|
10
9
|
|
|
11
10
|
const packageJson = JSON.parse(
|
|
12
11
|
await readFile(new URL('../package.json', import.meta.url), 'utf8'),
|
|
13
12
|
)
|
|
14
13
|
|
|
15
14
|
export const INTEGRATION_VERSION = packageJson.version
|
|
15
|
+
export const DSH_VERSION = DSH_RUNTIME_VERSION
|
|
16
16
|
|
|
17
17
|
function requireValue(args, index, flag) {
|
|
18
18
|
const value = args[index + 1]
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export const NPM_PACKAGE_NAME = 'dsh-harbor-evolution'
|
|
2
|
+
export const NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`
|
|
3
|
+
export const RELEASES_URL = 'https://github.com/istarwyh/harbor-self-evolving/releases'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1_000
|
|
6
|
+
const DEFAULT_FAILURE_TTL_MS = 5 * 60 * 1_000
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 2_500
|
|
8
|
+
|
|
9
|
+
export function parseSemver(value) {
|
|
10
|
+
const match = String(value ?? '').match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/)
|
|
11
|
+
if (!match) return undefined
|
|
12
|
+
return {
|
|
13
|
+
major: Number(match[1]),
|
|
14
|
+
minor: Number(match[2]),
|
|
15
|
+
patch: Number(match[3]),
|
|
16
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compareIdentifier(left, right) {
|
|
21
|
+
const leftNumber = /^\d+$/.test(left) ? Number(left) : undefined
|
|
22
|
+
const rightNumber = /^\d+$/.test(right) ? Number(right) : undefined
|
|
23
|
+
if (leftNumber !== undefined && rightNumber !== undefined) return Math.sign(leftNumber - rightNumber)
|
|
24
|
+
if (leftNumber !== undefined) return -1
|
|
25
|
+
if (rightNumber !== undefined) return 1
|
|
26
|
+
return left.localeCompare(right)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function compareSemver(leftValue, rightValue) {
|
|
30
|
+
const left = parseSemver(leftValue)
|
|
31
|
+
const right = parseSemver(rightValue)
|
|
32
|
+
if (!left || !right) return undefined
|
|
33
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
34
|
+
if (left[key] !== right[key]) return Math.sign(left[key] - right[key])
|
|
35
|
+
}
|
|
36
|
+
if (!left.prerelease.length && !right.prerelease.length) return 0
|
|
37
|
+
if (!left.prerelease.length) return 1
|
|
38
|
+
if (!right.prerelease.length) return -1
|
|
39
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length)
|
|
40
|
+
for (let index = 0; index < length; index += 1) {
|
|
41
|
+
if (left.prerelease[index] === undefined) return -1
|
|
42
|
+
if (right.prerelease[index] === undefined) return 1
|
|
43
|
+
const comparison = compareIdentifier(left.prerelease[index], right.prerelease[index])
|
|
44
|
+
if (comparison) return Math.sign(comparison)
|
|
45
|
+
}
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shellQuote(value) {
|
|
50
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderUpdateCommand(latestVersion, projectRoot) {
|
|
54
|
+
if (!parseSemver(latestVersion)) throw new Error('latestVersion must be a valid semantic version')
|
|
55
|
+
return `npx --yes ${NPM_PACKAGE_NAME}@${latestVersion} setup --project-root ${shellQuote(projectRoot)}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildResult(currentVersion, latestVersion, projectRoot, checkedAt, options = {}) {
|
|
59
|
+
const comparison = compareSemver(currentVersion, latestVersion)
|
|
60
|
+
if (comparison === undefined) {
|
|
61
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: options.source, stale: options.stale }
|
|
62
|
+
}
|
|
63
|
+
const updateAvailable = comparison < 0
|
|
64
|
+
return {
|
|
65
|
+
status: updateAvailable ? 'update-available' : 'up-to-date',
|
|
66
|
+
currentVersion,
|
|
67
|
+
latestVersion,
|
|
68
|
+
checkedAt,
|
|
69
|
+
source: options.source,
|
|
70
|
+
stale: options.stale,
|
|
71
|
+
releaseUrl: updateAvailable ? `${RELEASES_URL}/tag/v${latestVersion}` : RELEASES_URL,
|
|
72
|
+
command: updateAvailable ? renderUpdateCommand(latestVersion, projectRoot) : undefined,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createVersionChecker(options = {}) {
|
|
77
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch
|
|
78
|
+
const now = options.now ?? Date.now
|
|
79
|
+
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS
|
|
80
|
+
const failureTtlMs = options.failureTtlMs ?? DEFAULT_FAILURE_TTL_MS
|
|
81
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
82
|
+
let successful
|
|
83
|
+
let failedAt = 0
|
|
84
|
+
|
|
85
|
+
return async function checkVersion({ currentVersion, projectRoot, refresh = false }) {
|
|
86
|
+
const checkedAt = new Date(now()).toISOString()
|
|
87
|
+
if (!refresh && successful && now() < successful.expiresAt) {
|
|
88
|
+
return buildResult(currentVersion, successful.latestVersion, projectRoot, successful.checkedAt, { source: 'cache' })
|
|
89
|
+
}
|
|
90
|
+
if (!refresh && !successful && failedAt && now() - failedAt < failureTtlMs) {
|
|
91
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: 'cache' }
|
|
92
|
+
}
|
|
93
|
+
if (typeof fetchImpl !== 'function') {
|
|
94
|
+
failedAt = now()
|
|
95
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: 'host' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const controller = new AbortController()
|
|
99
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetchImpl(NPM_LATEST_URL, {
|
|
102
|
+
headers: { accept: 'application/json' },
|
|
103
|
+
redirect: 'error',
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
})
|
|
106
|
+
if (!response.ok) throw new Error('registry request failed')
|
|
107
|
+
const manifest = await response.json()
|
|
108
|
+
if (manifest?.name !== NPM_PACKAGE_NAME || !parseSemver(manifest.version)) {
|
|
109
|
+
throw new Error('registry response is invalid')
|
|
110
|
+
}
|
|
111
|
+
successful = {
|
|
112
|
+
latestVersion: manifest.version,
|
|
113
|
+
checkedAt,
|
|
114
|
+
expiresAt: now() + cacheTtlMs,
|
|
115
|
+
}
|
|
116
|
+
failedAt = 0
|
|
117
|
+
return buildResult(currentVersion, successful.latestVersion, projectRoot, checkedAt, { source: 'registry' })
|
|
118
|
+
} catch {
|
|
119
|
+
failedAt = now()
|
|
120
|
+
if (successful) {
|
|
121
|
+
return buildResult(currentVersion, successful.latestVersion, projectRoot, successful.checkedAt, { source: 'cache', stale: true })
|
|
122
|
+
}
|
|
123
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: 'registry' }
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timeout)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -8,6 +8,8 @@ export const COMPARE_ROUTE = '/_dsh/harbor-evolution/compare'
|
|
|
8
8
|
export const GOVERNANCE_ROUTE = '/_dsh/harbor-evolution/governance'
|
|
9
9
|
export const EVALUATOR_ROUTE = '/_dsh/harbor-evolution/evaluator'
|
|
10
10
|
export const META_ROUTE = '/_dsh/harbor-evolution/meta'
|
|
11
|
+
export const PROJECT_ROOT_ROUTE = '/_dsh/harbor-evolution/project-root'
|
|
12
|
+
export const VERSION_ROUTE = '/_dsh/harbor-evolution/version'
|
|
11
13
|
const MAX_MUTATION_BYTES = 256 * 1024
|
|
12
14
|
|
|
13
15
|
function sendJson(response, status, body) {
|
|
@@ -64,7 +66,7 @@ export function createApiHandler(load, code = 'request-failed') {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export function createDashboardHandler(service) {
|
|
67
|
-
return createApiHandler(
|
|
69
|
+
return createApiHandler(args => service.dashboard(args), 'dashboard-unavailable')
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function createMutationHandler(update, code = 'update-failed') {
|
|
@@ -116,6 +118,8 @@ export function installDashboardWeb(ctx, service) {
|
|
|
116
118
|
[GOVERNANCE_ROUTE, createApiHandler(args => service.governance(args), 'governance-unavailable')],
|
|
117
119
|
[EVALUATOR_ROUTE, createMutationHandler(args => service.evaluator(args), 'evaluator-update-failed')],
|
|
118
120
|
[META_ROUTE, createApiHandler(args => service.meta(args), 'meta-evaluation-unavailable')],
|
|
121
|
+
[VERSION_ROUTE, createApiHandler(args => service.version(args), 'version-check-unavailable')],
|
|
122
|
+
[PROJECT_ROOT_ROUTE, createMutationHandler(args => service.setProjectRoot(args), 'project-root-update-failed')],
|
|
119
123
|
]
|
|
120
124
|
for (const [route, handler] of routes) {
|
|
121
125
|
webCtx.effect(() => webCtx.webServer.register({ kind: 'exact', path: route, handler }), `harbor-evolution: ${route}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-harbor-evolution",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
|
@@ -48,9 +48,14 @@
|
|
|
48
48
|
"platform": "web"
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
+
"harborEvolution": {
|
|
52
|
+
"runtimePolicy": "follow-latest",
|
|
53
|
+
"dshRuntimeVersion": "latest",
|
|
54
|
+
"candidateAcpPackage": "@deepseek-ai/dsh-acp-demo@latest"
|
|
55
|
+
},
|
|
51
56
|
"peerDependencies": {
|
|
52
|
-
"@deepseek-ai/dsh-skill": "
|
|
53
|
-
"@deepseek-ai/dsh-tools": "
|
|
57
|
+
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6"
|
|
54
59
|
},
|
|
55
60
|
"dependencies": {
|
|
56
61
|
"@deepseek-ai/schemastery": "3.18.1"
|