dsh-harbor-evolution 0.7.2 → 0.8.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Installable DeepSeek Harness Plugin + Skill for running stable Harbor evaluation and controlled Agent evolution loops, with a native DSH Web dashboard.
4
4
 
5
- The package gives DSH twelve strict Harbor tools, dedicated Tool cards, a nine-stage Evaluation Workbench, an installation Doctor, and the model- and user-invocable `evolve-agent-with-harbor` Skill. The Skill starts with four user-facing concepts—Dataset (what to test), Generator (who answers), Evaluator plus criteria (what good means), and Optimizer (who improves it)—then compiles accepted choices into the strict Evaluation Stack. It validates Dataset identity, checks Trial Lifecycle and Score Validity, governs independent Ground Truth meta-evaluation, diagnoses evidence provenance, limits each iteration to one controlled Candidate change, and invokes the Promotion Gate only as an explicit action.
5
+ The package gives DSH sixteen strict Harbor tools, dedicated Tool cards, a nine-stage Evaluation Workbench, an installation Doctor, and the model- and user-invocable `evolve-agent-with-harbor` Skill. The Skill starts with four user-facing concepts—Dataset (what to test), Generator (who answers), Evaluator plus criteria (what good means), and Optimizer (who improves it)—then compiles accepted choices into the strict Evaluation Stack. When no Dataset is supplied, it can instead preview recent completed DSH Sessions and evaluate each immutable Session as one Historical Trial without rerunning a Candidate. A DSH Generator may explicitly pin the current default model as a non-secret Candidate identity while retaining the per-Job Host Broker credential boundary. The Plugin validates Dataset identity, checks Trial Lifecycle and Score Validity, governs independent Ground Truth meta-evaluation, diagnoses evidence provenance, limits each iteration to one controlled Candidate change, and invokes the Promotion Gate only as an explicit action.
6
6
 
7
7
  ## Install
8
8
 
@@ -14,8 +14,8 @@ npx --yes dsh-harbor-evolution@latest setup --project-root "$PWD"
14
14
 
15
15
  The setup command installs both required runtimes:
16
16
 
17
- - `harbor-dsh-evolution==0.7.2` in a managed Python environment.
18
- - `dsh-harbor-evolution@0.7.2` in the selected DSH profile.
17
+ - `harbor-dsh-evolution==0.8.0` in a managed Python environment.
18
+ - `dsh-harbor-evolution@0.8.0` in the selected DSH profile.
19
19
 
20
20
  It then stores the absolute Harbor executable paths and a fallback `projectRoot` in the profile's `harbor-evolution` block and verifies the integration. Agent Tool calls always use the calling session's absolute working directory as their project root; the configured value remains the Web Workbench and non-Agent fallback. Existing unrelated profile entries are preserved, and rerunning setup updates the same block.
21
21
 
@@ -37,12 +37,20 @@ Users may provide a single Query or Dataset path, a Generator curl or local Agen
37
37
  The Plugin registers:
38
38
 
39
39
  - `harbor_candidate_snapshot`
40
+ - `harbor_model_binding`
40
41
  - `harbor_evolution_init`
41
42
  - `harbor_evolution_doctor`
43
+ - `harbor_quick_diagnostic_init`
44
+ - `harbor_session_diagnostic_preview`
45
+ - `harbor_session_diagnostic_run`
42
46
  - `harbor_dataset_validate`
43
47
  - `harbor_context_preview`
44
48
  - `harbor_eval_run`
45
49
  - `harbor_eval_result`
50
+ - `harbor_evaluator_inspect`
51
+ - `harbor_evaluator_update`
52
+ - `harbor_ground_truth_init`
53
+ - `harbor_evaluator_meta_evaluate`
46
54
  - `harbor_candidate_compare`
47
55
 
48
56
  In the `web` profile, the same package also registers:
@@ -51,7 +59,7 @@ In the `web` profile, the same package also registers:
51
59
  - descriptor-authorized Evaluator/Rubric source editing for `script` and `llm-as-judge` implementations, with optimistic concurrency and mandatory new identities;
52
60
  - a `harbor-dsh-evaluator/v1` interface shared by deterministic scripts and LLM-as-Judge implementations;
53
61
  - compact result cards for all Harbor Tool calls;
54
- - a `Harbor Evolution` Settings section that checks the configured project, Evaluation Stack, Jobs directory, and CLI paths.
62
+ - a `Harbor Evolution` Settings section that checks the configured project, Evaluation Stack, Jobs directory, and CLI paths, supports process-local `projectRoot` reload, and checks npm for a newer formal release without silently installing it.
55
63
 
56
64
  The Web UI is intentionally read-only. Starting an evaluation or deciding promotion remains an explicit Agent + Skill workflow, so a page refresh can never launch an expensive Job.
57
65
 
@@ -63,6 +71,10 @@ Before each Job, the Plugin snapshots the current DSH Agent selection—provider
63
71
 
64
72
  `harbor_eval_run`, `harbor_context_preview`, and `harbor_evolution_doctor` inherit that selection by default. Advanced callers can override `candidateProvider` and `candidateModel` only as a pair, plus an optional `candidateReasoningEffort`. `openai-codex` performs a GPT Auth sign-in check before Harbor starts. The resulting model binding is part of Context v2 comparison identity, so any provider/model/reasoning change requires a new baseline.
65
73
 
74
+ `harbor_model_binding` returns the current default selection as a credential-free `model-binding.json` draft. Once included before Candidate snapshot, it enters the Candidate digest and becomes the required Job model identity. Conflicting Job or Plugin overrides fail before Harbor starts. Even for `openai-codex`, the Candidate receives only the short-lived Broker capability—never the Host OAuth file or an upstream API key.
75
+
76
+ When Settings opens, the Host performs a bounded npm registry check and caches successful results. An available release is shown with its exact installer command and release link. The browser never installs, rewrites a DSH profile, or restarts DSH; registry failures are non-blocking.
77
+
66
78
  `harbor_eval_result` defaults to the stable Summary. Use `view=job`, `view=dataset`, `view=progress`, `view=trial` plus a returned `trialId`, or `view=governance` to inspect sanitized instructions, generated output, evidence, and evaluator source without coupling the Agent to artifact file paths.
67
79
 
68
80
  ## What setup writes
package/index.js CHANGED
@@ -6,7 +6,10 @@ import { fileURLToPath } from 'node:url'
6
6
 
7
7
  import { loadBundledSkill } from './lib/official-skill.js'
8
8
  import { CandidateModelRuntime } from './lib/model-runtime.js'
9
+ import { RUNTIME_POLICY } from './lib/runtime-identity.js'
10
+ import { SessionDiagnosticService } from './lib/session-diagnostic.js'
9
11
  import { EvolutionService } from './lib/service.js'
12
+ import { runHistoricalEvaluation } from './lib/evolution.js'
10
13
  import { installDashboardWeb } from './lib/web.js'
11
14
 
12
15
  export const name = 'harbor-evolution'
@@ -26,9 +29,10 @@ export const Config = Schema.object({
26
29
  jobsDir: Schema.string().default('jobs'),
27
30
  harborBin: Schema.string().default(''),
28
31
  harborDshBin: Schema.string().default(''),
29
- dshVersion: Schema.string().default('0.1.0-rc.6'),
30
32
  agentImportPath: Schema.string().default('harbor_dsh_evolution.agent:DshCandidateAgent'),
31
33
  pluginImportPath: Schema.string().default('dsh-evolution'),
34
+ historicalAgentImportPath: Schema.string().default('harbor_dsh_evolution.session_agent:SessionObservationAgent'),
35
+ historicalPluginImportPath: Schema.string().default('dsh-historical-evaluation'),
32
36
  pythonPath: Schema.string().default(''),
33
37
  timeoutMs: Schema.number().default(1800000),
34
38
  candidateProvider: Schema.string().default(''),
@@ -38,6 +42,8 @@ export const Config = Schema.object({
38
42
  modelBrokerAdvertisedHost: Schema.string().default('host.docker.internal'),
39
43
  modelBrokerMaxRequests: Schema.number().min(1).default(1000),
40
44
  modelBrokerMaxRequestBytes: Schema.number().min(1024).default(33554432),
45
+ sessionMaxReads: Schema.number().min(1).default(100),
46
+ sessionReadConcurrency: Schema.number().min(1).default(4),
41
47
  })
42
48
 
43
49
  function jsonTool(definition, execute) {
@@ -61,9 +67,16 @@ function toolProjectRoot(exec) {
61
67
  return path.resolve(cwd)
62
68
  }
63
69
 
70
+ export function synchronizeWorkbenchProjectRoot(service, exec) {
71
+ const projectRoot = toolProjectRoot(exec)
72
+ service.activateProjectRoot(projectRoot, 'agent-session')
73
+ return projectRoot
74
+ }
75
+
64
76
  export function apply(ctx, config) {
65
77
  const resolved = {
66
78
  ...config,
79
+ runtimePolicy: RUNTIME_POLICY,
67
80
  projectRoot: path.resolve(config.projectRoot),
68
81
  harborBin: config.harborBin || process.env.HARBOR_BIN || checkoutExecutable('harbor'),
69
82
  harborDshBin: config.harborDshBin || process.env.HARBOR_DSH_BIN || checkoutExecutable('harbor-dsh'),
@@ -74,12 +87,18 @@ export function apply(ctx, config) {
74
87
  ),
75
88
  }
76
89
  const modelRuntime = new CandidateModelRuntime(ctx, resolved)
77
- const metadata = { pluginVersion: packageJson.version }
90
+ const metadata = { pluginVersion: packageJson.version, projectRootSource: 'configured' }
78
91
  const service = new EvolutionService(resolved, metadata, modelRuntime)
79
- const serviceForTool = exec => new EvolutionService({
80
- ...resolved,
81
- projectRoot: toolProjectRoot(exec),
82
- }, metadata, modelRuntime)
92
+ const sessionDiagnostic = new SessionDiagnosticService({
93
+ ctx,
94
+ config: resolved,
95
+ modelRuntime,
96
+ runHistoricalEvaluation,
97
+ })
98
+ const serviceForTool = exec => {
99
+ const projectRoot = synchronizeWorkbenchProjectRoot(service, exec)
100
+ return new EvolutionService({ ...resolved, projectRoot }, metadata, modelRuntime)
101
+ }
83
102
 
84
103
  ctx.skills.register(loadBundledSkill())
85
104
  installDashboardWeb(ctx, service)
@@ -94,11 +113,18 @@ export function apply(ctx, config) {
94
113
  },
95
114
  }, (args, exec) => serviceForTool(exec).snapshot(args)))
96
115
 
116
+ ctx.tools.register(jsonTool({
117
+ name: 'harbor_model_binding',
118
+ description: 'Freeze the current DSH default provider, model, and reasoning identity into a non-secret model-binding.json draft. Runtime access still uses the short-lived Host Model Broker capability.',
119
+ parameters: {},
120
+ }, (_args, exec) => serviceForTool(exec).modelBinding()))
121
+
97
122
  ctx.tools.register(jsonTool({
98
123
  name: 'harbor_evolution_init',
99
124
  description: 'Compile an accepted Dataset, Generator, Evaluator/criteria, and Optimizer onboarding card into a strict, non-overwriting Evaluation Stack project. Detailed identity fields are internal tool inputs, not a user questionnaire.',
100
125
  parameters: {
101
126
  datasetPath: { type: 'string', required: true },
127
+ workspaceSubdir: { type: 'string', description: 'Optional namespace under the current project root. Defaults to the project root; use it to host multiple independent Harbor projects.' },
102
128
  stackId: { type: 'string', required: true },
103
129
  stackVersion: { type: 'string', required: true },
104
130
  datasetId: { type: 'string', required: true },
@@ -131,6 +157,44 @@ export function apply(ctx, config) {
131
157
  },
132
158
  }, (args, exec) => serviceForTool(exec).doctor(args)))
133
159
 
160
+ ctx.tools.register(jsonTool({
161
+ name: 'harbor_quick_diagnostic_init',
162
+ description: 'Create a non-overwriting Harbor 1.4 wiring diagnostic with one Query, a minimal Host-model Candidate, a runnable Task, and an explicit non-promotion Evaluator. The supplied Rubric is recorded as a draft but is not treated as executed.',
163
+ parameters: {
164
+ query: { type: 'string', required: true },
165
+ rubric: { type: 'string', required: true },
166
+ workspaceSubdir: { type: 'string', description: 'Defaults to harbor-diagnostic under the current Agent session directory.' },
167
+ },
168
+ }, (args, exec) => serviceForTool(exec).quickDiagnostic(args)))
169
+
170
+ ctx.tools.register(jsonTool({
171
+ name: 'harbor_session_diagnostic_preview',
172
+ description: 'Preview up to 10 recently active, completed top-level DSH Sessions in the exact current workspace as an observe-existing Historical Generation Job. Returns only safe metadata and an owner-bound 15-minute selection token; it never exposes raw Session ids or transcript/tool payloads.',
173
+ parameters: {
174
+ limit: { type: 'number', description: 'Number of eligible recent Sessions, from 1 to 10. Defaults to 10.' },
175
+ createdAfter: { type: 'string', description: 'Optional ISO-8601 lower bound on Session creation time. Use it to narrow an exact scan when the workspace exceeds the configured read budget.' },
176
+ includeFeedback: { type: 'boolean', description: 'Include only feedback counts in Preview and redacted feedback in the frozen Batch. Defaults to true.' },
177
+ evaluatorProvider: { type: 'string', description: 'Optional Judge provider; supply together with evaluatorModel. Defaults to the calling DSH Agent model and is frozen into the confirmation token.' },
178
+ evaluatorModel: { type: 'string' },
179
+ evaluatorReasoningEffort: { type: 'string', description: 'Optional Judge reasoning effort; requires explicit evaluatorProvider and evaluatorModel.' },
180
+ },
181
+ }, (args, exec) => {
182
+ synchronizeWorkbenchProjectRoot(service, exec)
183
+ return sessionDiagnostic.preview(args, exec)
184
+ }))
185
+
186
+ ctx.tools.register(jsonTool({
187
+ name: 'harbor_session_diagnostic_run',
188
+ description: 'Consume a confirmed Session selection token, revalidate every immutable source boundary, write a private redacted Historical Generation Batch, materialize one Harbor Trial per Session, and run the non-promotion Historical Job.',
189
+ parameters: {
190
+ selectionToken: { type: 'string', required: true },
191
+ jobName: { type: 'string' },
192
+ },
193
+ }, (args, exec) => {
194
+ synchronizeWorkbenchProjectRoot(service, exec)
195
+ return sessionDiagnostic.run(args, exec)
196
+ }))
197
+
134
198
  ctx.tools.register(jsonTool({
135
199
  name: 'harbor_dataset_validate',
136
200
  description: 'Validate dataset-manifest.json, task uniqueness, instructions, paths, sensitive metadata, and the immutable source digest.',
@@ -211,6 +275,7 @@ export function apply(ctx, config) {
211
275
  description: 'Create a non-overwriting Ground Truth draft for evaluator meta-evaluation. GT may be human, programmatic, consensus, model, or external, but must have explicit provenance and remain independent of the Candidate evaluator.',
212
276
  parameters: {
213
277
  outputPath: { type: 'string', description: 'Defaults to .harbor/ground-truth.json' },
278
+ evaluationRoot: { type: 'string', description: 'Optional evaluation workspace root used to register custom Ground Truth paths.' },
214
279
  groundTruthId: { type: 'string', required: true },
215
280
  version: { type: 'string', required: true },
216
281
  sourceKind: { type: 'string', required: true, description: 'human, programmatic, consensus, model, or external' },
@@ -227,6 +292,7 @@ export function apply(ctx, config) {
227
292
  groundTruthPath: { type: 'string', description: 'Defaults to .harbor/ground-truth.json' },
228
293
  observationsPath: { type: 'string', required: true },
229
294
  outputPath: { type: 'string', description: 'Defaults to .harbor/meta-evaluation-report.json' },
295
+ evaluationRoot: { type: 'string', description: 'Optional evaluation workspace root used to register custom report paths.' },
230
296
  },
231
297
  }, (args, exec) => serviceForTool(exec).evaluatorMetaEvaluate(args)))
232
298
 
package/lib/candidate.js CHANGED
@@ -2,7 +2,10 @@ import { createHash } from 'node:crypto'
2
2
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
 
5
+ import { CANDIDATE_ACP_PACKAGE, DSH_RUNTIME_VERSION, RUNTIME_POLICY } from './runtime-identity.js'
6
+
5
7
  export const MANIFEST_NAME = 'candidate-manifest.json'
8
+ export const MODEL_BINDING_NAME = 'model-binding.json'
6
9
  const DIGEST_PREFIX = Buffer.from('harbor-dsh-candidate-v1\0')
7
10
  const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.harbor-runtime'])
8
11
  const EXCLUDED_FILES = new Set([MANIFEST_NAME, '.DS_Store'])
@@ -16,6 +19,7 @@ const CREDENTIAL_FILES = new Set([
16
19
  'id_rsa',
17
20
  'id_ed25519',
18
21
  ])
22
+ const MODEL_BINDING_KEYS = new Set(['schema_version', 'source', 'provider', 'model', 'reasoning_effort'])
19
23
 
20
24
  async function walk(root, current = root) {
21
25
  const entries = await readdir(current, { withFileTypes: true })
@@ -33,6 +37,45 @@ async function walk(root, current = root) {
33
37
  return files.sort((a, b) => Buffer.compare(Buffer.from(a.relative), Buffer.from(b.relative)))
34
38
  }
35
39
 
40
+ export async function loadModelBinding(candidateDir) {
41
+ const pathname = path.join(path.resolve(candidateDir), MODEL_BINDING_NAME)
42
+ let value
43
+ try {
44
+ value = JSON.parse(await readFile(pathname, 'utf8'))
45
+ } catch (error) {
46
+ if (error.code === 'ENOENT') return undefined
47
+ if (error instanceof SyntaxError) throw new Error(`${MODEL_BINDING_NAME} is not valid JSON`)
48
+ throw error
49
+ }
50
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
51
+ throw new Error(`${MODEL_BINDING_NAME} must be an object`)
52
+ }
53
+ const unknown = Object.keys(value).filter(key => !MODEL_BINDING_KEYS.has(key)).sort()
54
+ if (unknown.length) {
55
+ throw new Error(`${MODEL_BINDING_NAME} contains unsupported or secret-bearing fields: ${unknown.join(', ')}`)
56
+ }
57
+ if (value.schema_version !== 1) throw new Error(`${MODEL_BINDING_NAME} requires schema_version=1`)
58
+ const source = typeof value.source === 'string' ? value.source.trim() : ''
59
+ const provider = typeof value.provider === 'string' ? value.provider.trim() : ''
60
+ const model = typeof value.model === 'string' ? value.model.trim() : ''
61
+ if (!source || !provider || !model) {
62
+ throw new Error(`${MODEL_BINDING_NAME} requires non-empty source, provider, and model`)
63
+ }
64
+ const reasoningEffort = value.reasoning_effort === undefined
65
+ ? undefined
66
+ : typeof value.reasoning_effort === 'string' ? value.reasoning_effort.trim() : ''
67
+ if (value.reasoning_effort !== undefined && !reasoningEffort) {
68
+ throw new Error(`${MODEL_BINDING_NAME} reasoning_effort must be a non-empty string when present`)
69
+ }
70
+ return {
71
+ schema_version: 1,
72
+ source,
73
+ provider,
74
+ model,
75
+ ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
76
+ }
77
+ }
78
+
36
79
  export async function computeCandidate(candidateDir) {
37
80
  const root = path.resolve(candidateDir)
38
81
  if (!(await stat(root)).isDirectory()) throw new Error(`Candidate path is not a directory: ${root}`)
@@ -83,6 +126,7 @@ async function validateCandidateContract(root) {
83
126
  if (credentialPaths.length > 0) {
84
127
  throw new Error(`Candidate contains credential-bearing files: ${credentialPaths.join(', ')}; inject credentials at runtime instead`)
85
128
  }
129
+ await loadModelBinding(root)
86
130
  }
87
131
 
88
132
  export async function snapshotCandidate(candidateDir, options = {}) {
@@ -96,11 +140,18 @@ export async function snapshotCandidate(candidateDir, options = {}) {
96
140
  }
97
141
  const candidateId = options.candidateId ?? packageJson.name
98
142
  const version = options.version ?? packageJson.version
99
- const runtimeVersion = options.runtimeVersion ?? '0.1.0-rc.6'
100
- if (!candidateId || !version || !runtimeVersion) {
101
- throw new Error('Candidate id, version, and runtime version must not be empty; set package.json name/version or pass explicit values')
143
+ if (!candidateId || !version) {
144
+ throw new Error('Candidate id and version must not be empty; set package.json name/version or pass explicit values')
102
145
  }
103
146
  const computed = await computeCandidate(root)
147
+ const metadata = { ...(options.metadata ?? {}) }
148
+ const modelBinding = await loadModelBinding(root)
149
+ if (modelBinding) {
150
+ if (metadata.model_binding !== undefined && JSON.stringify(metadata.model_binding) !== JSON.stringify(modelBinding)) {
151
+ throw new Error('Candidate metadata model_binding must match model-binding.json')
152
+ }
153
+ metadata.model_binding = modelBinding
154
+ }
104
155
  const manifest = {
105
156
  schema_version: 1,
106
157
  candidate_id: String(candidateId),
@@ -109,11 +160,13 @@ export async function snapshotCandidate(candidateDir, options = {}) {
109
160
  created_at: new Date().toISOString(),
110
161
  runtime: {
111
162
  kind: 'deepseek-harness',
112
- version: String(runtimeVersion),
163
+ policy: RUNTIME_POLICY,
164
+ version: DSH_RUNTIME_VERSION,
165
+ package: CANDIDATE_ACP_PACKAGE,
113
166
  transport: 'acp',
114
167
  },
115
168
  files: computed.files,
116
- metadata: options.metadata ?? {},
169
+ metadata,
117
170
  }
118
171
  await mkdir(root, { recursive: true })
119
172
  await writeFile(path.join(root, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}\n`)