@foggy-projects/deepseek-harness-plugin 0.4.0-beta.3 → 0.4.0-beta.5

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
@@ -8,7 +8,7 @@ only when the user selects **Initialize Foggy**.
8
8
  ## Local beta installation
9
9
 
10
10
  ```powershell
11
- dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.3.tgz
11
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.5.tgz
12
12
  ```
13
13
 
14
14
  Restart `dsh web`, open Settings → Plugins → Foggy Data Analysis, and initialize
@@ -20,13 +20,27 @@ After npm beta publication, the corresponding one-line install is:
20
20
  dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-plugin@beta
21
21
  ```
22
22
 
23
- For development, `FOGGY_PROJECT_ROOT` can override the workspace that receives
24
- the Skills and `FOGGY_ASSET_CACHE_DIRS` can contain platform-delimited verified
25
- asset-cache directories. Neither variable is required for a normal install.
23
+ For development, `FOGGY_ASSET_CACHE_DIRS` can contain platform-delimited verified
24
+ asset-cache directories. It is not required for a normal install.
26
25
 
27
26
  Database credentials are deliberately outside the ordinary DSH settings
28
27
  document. The database and semantic-layer wizard is the next Bundle milestone.
29
28
 
29
+ ## Native Skill and workspace contract
30
+
31
+ The Bundle registers `foggy-deepseek-onboarding` and the downloaded
32
+ `foggy-ai-analysis` through DeepSeek Harness's native Skill provider API. Skills
33
+ are available in every DSH workspace without copying or symlinking `.agents`.
34
+ The current session `cwd` remains the workspace boundary for semantic drafts and
35
+ evidence.
36
+
37
+ CLI, Launcher, the analysis Skill, install state, and Runtime state live in the
38
+ user-level Foggy component directories. The managed CLI is intentionally isolated
39
+ and does not need to be on `PATH`. **Re-download / Repair** verifies the global
40
+ analysis Skill, backs up modified or outdated managed content, restores it, and
41
+ invalidates DSH's Skill catalog. The onboarding Skill is bundled with the plugin
42
+ and is restored by reinstalling or upgrading the plugin package.
43
+
30
44
  ## Linux and WSL2 experience
31
45
 
32
46
  Ubuntu and WSL2 users can use the checked-in preflighted installer under
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  DSH_VERSION="0.1.1-rc.2"
5
5
  PNPM_VERSION="11.7.0"
6
- PLUGIN_VERSION="0.4.0-beta.3"
6
+ PLUGIN_VERSION="0.4.0-beta.4"
7
7
  PLUGIN_REF="v${PLUGIN_VERSION}"
8
8
  PLUGIN_REPOSITORY="https://github.com/foggy-projects/foggy-deepseek-harness-plugin.git"
9
9
 
package/lib/client.js CHANGED
@@ -41,6 +41,8 @@ window.__ModuleLoader__.load({
41
41
  cli: 'CLI',
42
42
  launcher: 'Launcher',
43
43
  analysisSkill: '分析 Skill',
44
+ onboardingSkill: '引导 Skill',
45
+ skillRegistry: 'DSH Skill 注册',
44
46
  python: 'Python',
45
47
  java: 'Java',
46
48
  installed: '已安装',
@@ -57,7 +59,7 @@ window.__ModuleLoader__.load({
57
59
  progressCli: '准备 CLI',
58
60
  progressLauncher: '准备 Launcher',
59
61
  progressAnalysis: '准备分析 Skill',
60
- progressSkills: '安装工作区 Skills',
62
+ progressSkills: '注册 DSH Skills',
61
63
  progressState: '写入安装状态',
62
64
  progressComplete: '初始化完成',
63
65
  progressFiles: '文件',
@@ -67,9 +69,8 @@ window.__ModuleLoader__.load({
67
69
  installRoot: '组件目录',
68
70
  dataRoot: '数据目录',
69
71
  runtimeUrl: 'Runtime 地址',
70
- projectRoot: 'Skill 目标工作区',
71
72
  nextTitle: '后续配置',
72
- nextCopy: 'Runtime 启动成功后,将继续进入数据库连接与语义层向导。数据库密码不会写入 DSH 普通设置。',
73
+ nextCopy: 'Skills 已通过 DeepSeek Harness 原生注册表提供给所有工作区;每个会话直接使用自己的工作目录。Runtime 启动成功后,将继续进入数据库连接与语义层向导。',
73
74
  beta: 'Beta',
74
75
  }
75
76
 
@@ -88,6 +89,8 @@ window.__ModuleLoader__.load({
88
89
  cli: 'CLI',
89
90
  launcher: 'Launcher',
90
91
  analysisSkill: 'Analysis Skill',
92
+ onboardingSkill: 'Onboarding Skill',
93
+ skillRegistry: 'DSH Skill registry',
91
94
  python: 'Python',
92
95
  java: 'Java',
93
96
  installed: 'Installed',
@@ -104,7 +107,7 @@ window.__ModuleLoader__.load({
104
107
  progressCli: 'Preparing CLI',
105
108
  progressLauncher: 'Preparing Launcher',
106
109
  progressAnalysis: 'Preparing analysis Skill',
107
- progressSkills: 'Installing workspace Skills',
110
+ progressSkills: 'Registering DSH Skills',
108
111
  progressState: 'Writing install state',
109
112
  progressComplete: 'Initialization complete',
110
113
  progressFiles: 'files',
@@ -114,9 +117,8 @@ window.__ModuleLoader__.load({
114
117
  installRoot: 'Components',
115
118
  dataRoot: 'Data',
116
119
  runtimeUrl: 'Runtime URL',
117
- projectRoot: 'Skill target workspace',
118
120
  nextTitle: 'Next configuration',
119
- nextCopy: 'After the Runtime starts, the database connection and semantic-layer wizard comes next. Database passwords are never stored in ordinary DSH settings.',
121
+ nextCopy: 'Skills are provided to every workspace through the native DeepSeek Harness registry, and each session uses its own working directory. After Runtime starts, the database connection and semantic-layer wizard comes next.',
120
122
  beta: 'Beta',
121
123
  }
122
124
 
@@ -264,8 +266,8 @@ window.__ModuleLoader__.load({
264
266
  jsxs('div', { className: 'foggy-state', children: [jsx('span', { className: 'foggy-dot', 'data-state': status.state }), jsx('span', { children: t(stateLabels[status.state] || 'notInstalled') })] }),
265
267
  jsxs('div', { className: 'foggy-actions', children: [
266
268
  jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: refresh, children: t('refresh') }),
267
- !status.installed ? jsx('button', { className: 'foggy-button foggy-button-primary', type: 'button', disabled: busy, onClick: () => run('initialize'), children: t('initialize') }) : null,
268
- status.installed ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('repair'), children: t('repair') }) : null,
269
+ status.state === 'not-installed' ? jsx('button', { className: 'foggy-button foggy-button-primary', type: 'button', disabled: busy, onClick: () => run('initialize'), children: t('initialize') }) : null,
270
+ status.state !== 'not-installed' ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('repair'), children: t('repair') }) : null,
269
271
  status.installed && !status.running && status.components.java.available ? jsx('button', { className: 'foggy-button foggy-button-primary', type: 'button', disabled: busy, onClick: () => run('runtimeStart'), children: t('start') }) : null,
270
272
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
271
273
  ] }),
@@ -278,13 +280,14 @@ window.__ModuleLoader__.load({
278
280
  componentCard(t('cli'), status.components.cli, 'runtime', t),
279
281
  componentCard(t('launcher'), status.components.launcher, 'runtime', t),
280
282
  componentCard(t('analysisSkill'), status.components.analysisSkill, 'runtime', t),
283
+ componentCard(t('onboardingSkill'), status.components.onboardingSkill, 'runtime', t),
284
+ componentCard(t('skillRegistry'), { installed: status.components.onboardingSkill?.provider === 'foggy-managed-skills', version: 'native' }, 'runtime', t),
281
285
  ] }),
282
286
  jsxs('div', { className: 'foggy-paths', children: [
283
287
  jsx('h4', { children: t('roots') }),
284
288
  jsxs('dl', { children: [
285
289
  jsx('dt', { children: t('installRoot') }), jsx('dd', { children: status.roots.installRoot }),
286
290
  jsx('dt', { children: t('dataRoot') }), jsx('dd', { children: status.roots.dataRoot }),
287
- jsx('dt', { children: t('projectRoot') }), jsx('dd', { children: status.projectRoot }),
288
291
  status.runtimeUrl ? jsx('dt', { children: t('runtimeUrl') }) : null,
289
292
  status.runtimeUrl ? jsx('dd', { children: status.runtimeUrl }) : null,
290
293
  ] }),
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ import { dirname, join, delimiter } from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  import { promisify } from 'node:util'
7
7
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
8
+ import { createFoggySkillProvider } from './skill-provider.js'
8
9
  import { compatible } from './version.js'
9
10
 
10
11
  const execFileAsync = promisify(execFile)
@@ -120,10 +121,18 @@ function operationView(operation) {
120
121
  }
121
122
 
122
123
  export class FoggyIntegrationGateway extends TypertRemoteService {
124
+ static inject = ['skills']
125
+
123
126
  operation = null
127
+ skillProviderControl = null
124
128
 
125
129
  constructor(ctx) {
126
130
  super(ctx, 'foggyIntegration')
131
+ const roots = defaultRoots()
132
+ ctx.skills.registerProvider((control) => {
133
+ this.skillProviderControl = control
134
+ return createFoggySkillProvider({ installRoot: roots.installRoot, versionsFile })
135
+ })
127
136
  for (const initialize of markerInitializers) initialize.call(this)
128
137
  }
129
138
 
@@ -147,6 +156,9 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
147
156
  try { runtime = await readJson(runtimeStatePath) } catch {}
148
157
  const cliPath = state?.cli?.command
149
158
  const launcherPath = state?.launcher?.path
159
+ const analysisSkillPath = state?.skills?.analysis?.path || join(roots.installRoot, 'skills', 'foggy-ai-analysis')
160
+ const onboardingSkillPath = join(packageRoot, 'skills', 'foggy-deepseek-onboarding')
161
+ const analysisMarker = await readOptionalJson(join(analysisSkillPath, '.foggy-managed-skill.json'))
150
162
  const components = {
151
163
  python,
152
164
  java,
@@ -159,11 +171,23 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
159
171
  version: state?.launcher?.version ?? manifest.components.launcher.version,
160
172
  },
161
173
  analysisSkill: {
162
- installed: Boolean(state?.skills?.analysis?.path && await exists(state.skills.analysis.path)),
174
+ installed: Boolean(
175
+ await exists(join(analysisSkillPath, 'SKILL.md'))
176
+ && analysisMarker?.schemaVersion === 'foggy-managed-skill/v1'
177
+ && analysisMarker?.componentVersion === manifest.components.analysisSkill.version
178
+ ),
163
179
  version: state?.skills?.analysis?.version ?? manifest.components.analysisSkill.version,
164
180
  },
181
+ onboardingSkill: {
182
+ installed: await exists(join(onboardingSkillPath, 'SKILL.md')),
183
+ version: manifest.packageVersion,
184
+ provider: 'foggy-managed-skills',
185
+ },
165
186
  }
166
- const installed = components.cli.installed && components.launcher.installed && components.analysisSkill.installed
187
+ const installed = components.cli.installed
188
+ && components.launcher.installed
189
+ && components.analysisSkill.installed
190
+ && components.onboardingSkill.installed
167
191
  const running = Boolean(runtime && processRunning(Number(runtime.pid)))
168
192
  const progress = await readOptionalJson(progressPath)
169
193
  let operation = operationView(this.operation)
@@ -189,7 +213,6 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
189
213
  running,
190
214
  runtimeUrl: running ? runtime.runtimeUrl ?? null : null,
191
215
  roots,
192
- projectRoot: process.env.FOGGY_PROJECT_ROOT || process.cwd(),
193
216
  components,
194
217
  operation,
195
218
  next: installed ? (running ? 'configure-database' : 'start-runtime') : 'initialize',
@@ -202,13 +225,14 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
202
225
  return {
203
226
  success: true,
204
227
  roots,
205
- projectRoot: process.env.FOGGY_PROJECT_ROOT || process.cwd(),
228
+ workspaceMode: 'dsh-session-cwd',
206
229
  versions: Object.fromEntries(Object.entries(manifest.components).map(([name, value]) => [name, value.version])),
207
230
  operations: [
208
231
  'create isolated Python environment',
209
232
  'download and verify pinned CLI and Launcher assets',
210
- 'install Foggy analysis and onboarding Skills into the workspace',
211
- 'write local install state',
233
+ 'install the Foggy analysis Skill into the global managed component directory',
234
+ 'register onboarding and analysis Skills through the native DSH Skill registry',
235
+ 'write global install state; resolve each workspace from the DSH session cwd',
212
236
  ],
213
237
  secretsInDshSettings: false,
214
238
  }
@@ -235,7 +259,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
235
259
  return { success: false, accepted: false, operation: operationView(this.operation), error: 'another Foggy operation is running' }
236
260
  }
237
261
  const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
238
- const args = explicitArgs ?? ['install', '--project-root', process.env.FOGGY_PROJECT_ROOT || process.cwd()]
262
+ const args = explicitArgs ?? ['install']
239
263
  if (!explicitArgs) {
240
264
  const roots = defaultRoots()
241
265
  args.push('--progress-file', join(roots.dataRoot, 'operation-progress.json'), '--operation-id', id, '--operation-kind', kind)
@@ -270,6 +294,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
270
294
  operation.state = result.success === false ? 'failed' : 'succeeded'
271
295
  operation.result = result
272
296
  operation.finishedAt = new Date().toISOString()
297
+ this.skillProviderControl?.invalidate()
273
298
  if (operation.progress) {
274
299
  operation.progress = {
275
300
  ...operation.progress,
@@ -284,6 +309,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
284
309
  operation.state = 'failed'
285
310
  operation.error = String(error.message ?? error)
286
311
  operation.finishedAt = new Date().toISOString()
312
+ this.skillProviderControl?.invalidate()
287
313
  if (operation.progress) {
288
314
  operation.progress = {
289
315
  ...operation.progress,
@@ -0,0 +1,111 @@
1
+ import { access, readFile } from 'node:fs/promises'
2
+ import { constants as fsConstants } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { BUNDLED_SKILL_RANK } from '@deepseek-ai/dsh-skill'
6
+
7
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
8
+ const onboardingRoot = join(packageRoot, 'skills', 'foggy-deepseek-onboarding')
9
+ const onboardingPath = join(onboardingRoot, 'SKILL.md')
10
+ const providerName = 'foggy-managed-skills'
11
+ const invocation = { modelInvocable: true, userInvocable: true }
12
+
13
+ async function exists(path) {
14
+ try {
15
+ await access(path, fsConstants.F_OK)
16
+ return true
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ async function readOptionalJson(path) {
23
+ try {
24
+ return JSON.parse(await readFile(path, 'utf8'))
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+
30
+ function parseSkill(source, expectedName) {
31
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
32
+ if (!match) throw new Error(`Foggy Skill ${expectedName} has no valid frontmatter`)
33
+ const metadata = Object.fromEntries(match[1].split(/\r?\n/).flatMap((line) => {
34
+ const separator = line.indexOf(':')
35
+ if (separator < 1) return []
36
+ return [[line.slice(0, separator).trim(), line.slice(separator + 1).trim().replace(/^(['\"])(.*)\1$/, '$2')]]
37
+ }))
38
+ if (metadata.name !== expectedName || !metadata.description) {
39
+ throw new Error(`Foggy Skill ${expectedName} has invalid name or description metadata`)
40
+ }
41
+ return { metadata, content: match[2] }
42
+ }
43
+
44
+ function candidate(name, description, path, source) {
45
+ const root = dirname(path)
46
+ return {
47
+ name,
48
+ description,
49
+ invocation,
50
+ provider: providerName,
51
+ source,
52
+ resourceBase: { kind: 'directory', path: root },
53
+ rank: BUNDLED_SKILL_RANK,
54
+ locator: { name, path },
55
+ path,
56
+ }
57
+ }
58
+
59
+ async function managedAnalysisSkill(installRoot, manifest) {
60
+ const state = await readOptionalJson(join(installRoot, 'install-state.json'))
61
+ const path = state?.skills?.analysis?.path
62
+ if (!path || !await exists(join(path, 'SKILL.md'))) return null
63
+ const marker = await readOptionalJson(join(path, '.foggy-managed-skill.json'))
64
+ if (
65
+ marker?.schemaVersion !== 'foggy-managed-skill/v1'
66
+ || marker?.kind !== 'analysis'
67
+ || marker?.componentVersion !== manifest.components.analysisSkill.version
68
+ ) return null
69
+ return join(path, 'SKILL.md')
70
+ }
71
+
72
+ export function createFoggySkillProvider({ installRoot, versionsFile }) {
73
+ return {
74
+ name: providerName,
75
+ async list(options = {}) {
76
+ if (options.signal?.aborted) return []
77
+ const manifest = await readOptionalJson(versionsFile)
78
+ const onboarding = parseSkill(await readFile(onboardingPath, 'utf8'), 'foggy-deepseek-onboarding')
79
+ const result = [candidate(
80
+ 'foggy-deepseek-onboarding',
81
+ onboarding.metadata.description,
82
+ onboardingPath,
83
+ 'bundled',
84
+ )]
85
+ if (!manifest) return result
86
+ const analysisPath = await managedAnalysisSkill(installRoot, manifest)
87
+ if (analysisPath) {
88
+ const analysis = parseSkill(await readFile(analysisPath, 'utf8'), 'foggy-ai-analysis')
89
+ result.push(candidate('foggy-ai-analysis', analysis.metadata.description, analysisPath, 'bundled'))
90
+ }
91
+ return result
92
+ },
93
+ async get(selected, options = {}) {
94
+ if (options.signal?.aborted) return undefined
95
+ const locator = selected?.locator
96
+ if (!locator?.name || !locator?.path || !await exists(locator.path)) return undefined
97
+ const parsed = parseSkill(await readFile(locator.path, 'utf8'), locator.name)
98
+ return {
99
+ name: locator.name,
100
+ description: parsed.metadata.description,
101
+ invocation,
102
+ provider: providerName,
103
+ source: selected.source,
104
+ resourceBase: { kind: 'directory', path: dirname(locator.path) },
105
+ content: parsed.content,
106
+ path: locator.path,
107
+ metadata: parsed.metadata,
108
+ }
109
+ },
110
+ }
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foggy-projects/deepseek-harness-plugin",
3
- "version": "0.4.0-beta.3",
3
+ "version": "0.4.0-beta.5",
4
4
  "description": "Foggy Java data analysis engine integration for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -50,10 +50,11 @@
50
50
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
51
51
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
52
52
  "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
53
+ "@deepseek-ai/dsh-skill": "^0.1.1-rc.2",
53
54
  "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2"
54
55
  },
55
56
  "scripts": {
56
- "check": "node --check lib/index.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
57
+ "check": "node --check lib/index.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
57
58
  "test": "node --test test/package.test.js"
58
59
  }
59
60
  }
@@ -7,6 +7,24 @@ description: Install and operate a pinned Foggy CLI-first dev/test environment f
7
7
 
8
8
  Set up Foggy through shell and `foggy-runtime` CLI. Do not configure Foggy MCP for this local workflow.
9
9
 
10
+ ## Native registry and installed-state gate
11
+
12
+ This Skill is registered by the Foggy plugin through DeepSeek Harness's native Skill registry. Its
13
+ resource base is the authoritative location for these scripts and references; do not search for or
14
+ copy this Skill into the current workspace.
15
+
16
+ - Resolve the global install state from the platform default component directory: on Linux use
17
+ `${XDG_DATA_HOME:-$HOME/.local/share}/foggy/deepseek-harness/install-state.json`; on Windows use
18
+ `%LOCALAPPDATA%\Foggy\DeepSeekHarness\install-state.json`.
19
+ - Confirm the installation with this Skill's `doctor` wrapper and pass the current DSH session
20
+ workspace as `--project-root`. The absence of `foggy-runtime` from `PATH` is not evidence that the
21
+ managed CLI is missing; the plugin intentionally installs it in an isolated environment and records
22
+ its absolute command in the global install state.
23
+ - Do not independently download or reinstall the CLI. If the global install state, managed marker, or
24
+ analysis Skill is missing or invalid, ask the user to open the Foggy plugin settings and use
25
+ **Re-download / Repair**. Repair restores the global managed analysis Skill and invalidates the
26
+ native DSH Skill catalog. Reinstall or upgrade the plugin itself to restore this bundled Skill.
27
+
10
28
  ## Mandatory orchestration boundary
11
29
 
12
30
  For every new-database onboarding session, this Skill is the orchestration authority until
@@ -42,13 +60,14 @@ For every new-database onboarding session, this Skill is the orchestration autho
42
60
 
43
61
  ## Workflow
44
62
 
45
- 1. Run `scripts/doctor.ps1 --project-root <root>` on Windows or
46
- `bash scripts/doctor.sh --project-root <root>` on Linux.
47
- 2. If the pinned CLI, Launcher, or analysis Skill is missing, run the matching `install` script. Use
48
- `--dry-run` first when paths or permissions are uncertain.
63
+ 1. Run `scripts/doctor.ps1 --project-root <current-session-workspace>` on Windows or
64
+ `bash scripts/doctor.sh --project-root <current-session-workspace>` on Linux.
65
+ 2. If the pinned CLI, Launcher, or global managed analysis Skill is missing, use the Foggy
66
+ plugin's Repair action. Use the matching install script only when the plugin UI is unavailable;
67
+ use `--dry-run` first when paths or permissions are uncertain.
49
68
  3. Run `runtime-start` and require successful `wait-ready` plus `capabilities`. Record engine,
50
69
  Runtime API version, schema version, security mode, URL, namespace, PID, and evidence path.
51
- 4. Confirm the project contains `.agents/skills/foggy-ai-analysis/SKILL.md`.
70
+ 4. Load `foggy-ai-analysis` from the native DSH Skill registry. Do not require a workspace copy.
52
71
  5. For a new business database, read [references/onboarding-workflow.md](references/onboarding-workflow.md)
53
72
  and prefer its two composite `onboard-datasource-run` / `onboard-semantic-run` commands. Require the
54
73
  trusted operator to create the private CLI profile outside Harness. Accept only the opaque profile
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "foggy-deepseek-onboarding-versions/v1",
3
- "packageVersion": "0.4.0-beta.3",
4
- "validatedAt": "2026-08-28",
3
+ "packageVersion": "0.4.0-beta.5",
4
+ "validatedAt": "2026-09-01",
5
5
  "components": {
6
6
  "deepseekHarness": {
7
7
  "version": "0.1.1-rc.2",
@@ -25,6 +25,9 @@ import zipfile
25
25
  STATE_SCHEMA = "foggy-deepseek-onboarding-install/v1"
26
26
  RUNTIME_STATE_SCHEMA = "foggy-deepseek-onboarding-runtime/v1"
27
27
  ONBOARDING_STATE_SCHEMA = "foggy-deepseek-onboarding-state/v1"
28
+ MANAGED_SKILL_SCHEMA = "foggy-managed-skill/v1"
29
+ MANAGED_SKILL_MARKER = ".foggy-managed-skill.json"
30
+ LEGACY_SKILL_MARKER = ".foggy-onboarding-install.json"
28
31
  CONNECTION_SCHEMA = "foggy-deepseek-connection/v1"
29
32
  SEMANTIC_PLAN_SCHEMA = "foggy-deepseek-semantic-plan/v1"
30
33
  PROFILE_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]{0,62}$")
@@ -331,6 +334,98 @@ def atomic_json(path: Path, payload: dict) -> None:
331
334
  os.replace(temporary, path)
332
335
 
333
336
 
337
+ def skill_tree_digest(root: Path) -> str:
338
+ """Hash managed Skill content while ignoring generated and management files."""
339
+ digest = hashlib.sha256()
340
+ ignored_names = {MANAGED_SKILL_MARKER, LEGACY_SKILL_MARKER}
341
+ files = sorted(
342
+ path for path in root.rglob("*")
343
+ if path.is_file()
344
+ and path.name not in ignored_names
345
+ and "__pycache__" not in path.parts
346
+ and path.suffix != ".pyc"
347
+ )
348
+ for path in files:
349
+ relative = path.relative_to(root).as_posix().encode("utf-8")
350
+ digest.update(len(relative).to_bytes(4, "big"))
351
+ digest.update(relative)
352
+ with path.open("rb") as stream:
353
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
354
+ digest.update(chunk)
355
+ return digest.hexdigest()
356
+
357
+
358
+ def read_skill_marker(destination: Path) -> dict | None:
359
+ for name in (MANAGED_SKILL_MARKER, LEGACY_SKILL_MARKER):
360
+ marker = destination / name
361
+ if marker.is_file():
362
+ try:
363
+ payload = json.loads(marker.read_text(encoding="utf-8"))
364
+ if isinstance(payload, dict):
365
+ return payload
366
+ except (OSError, json.JSONDecodeError):
367
+ return None
368
+ return None
369
+
370
+
371
+ def managed_skill_status(destination: Path, kind: str, expected_version: str) -> dict:
372
+ marker = read_skill_marker(destination)
373
+ present = (destination / "SKILL.md").is_file()
374
+ actual_digest = skill_tree_digest(destination) if destination.is_dir() else None
375
+ marker_digest = marker.get("installedDigest") if marker else None
376
+ managed = bool(
377
+ marker
378
+ and marker.get("schemaVersion") == MANAGED_SKILL_SCHEMA
379
+ and marker.get("kind") == kind
380
+ )
381
+ version = marker.get("componentVersion") if marker else None
382
+ return {
383
+ "path": str(destination),
384
+ "present": present,
385
+ "markerPresent": bool(marker),
386
+ "managed": managed,
387
+ "version": version,
388
+ "versionMatches": version == expected_version,
389
+ "integrityValid": bool(marker_digest and actual_digest == marker_digest),
390
+ "valid": bool(present and managed and version == expected_version and marker_digest and actual_digest == marker_digest),
391
+ }
392
+
393
+
394
+ def write_skill_marker(
395
+ destination: Path,
396
+ *,
397
+ kind: str,
398
+ package_version: str,
399
+ component_version: str,
400
+ source_digest: str,
401
+ archive_sha256: str | None = None,
402
+ ) -> dict:
403
+ marker = {
404
+ "schemaVersion": MANAGED_SKILL_SCHEMA,
405
+ "kind": kind,
406
+ "managedBy": "@foggy-projects/deepseek-harness-plugin",
407
+ "packageVersion": package_version,
408
+ "componentVersion": component_version,
409
+ "sourceDigest": source_digest,
410
+ "installedDigest": skill_tree_digest(destination),
411
+ "installedAt": now_utc(),
412
+ }
413
+ if archive_sha256:
414
+ marker["archiveSha256"] = archive_sha256
415
+ atomic_json(destination / MANAGED_SKILL_MARKER, marker)
416
+ legacy = destination / LEGACY_SKILL_MARKER
417
+ if legacy.is_file():
418
+ legacy.unlink()
419
+ return marker
420
+
421
+
422
+ def backup_skill(destination: Path, backup_root: Path) -> Path:
423
+ backup_root.mkdir(parents=True, exist_ok=True)
424
+ backup = backup_root / f"{destination.name}-{dt.datetime.now().strftime('%Y%m%d-%H%M%S-%f')}"
425
+ shutil.move(str(destination), str(backup))
426
+ return backup
427
+
428
+
334
429
  def read_json_object(path: Path, label: str) -> dict:
335
430
  if not path.is_file():
336
431
  raise OnboardingError(f"{label} not found: {path}")
@@ -621,22 +716,16 @@ def safe_extract(zip_path: Path, destination: Path) -> None:
621
716
  archive.extractall(destination)
622
717
 
623
718
 
624
- def install_analysis_skill(zip_path: Path, project_root: Path, version: str, expected_hash: str, replace: bool) -> dict:
625
- skills_root = project_root / ".agents" / "skills"
719
+ def install_analysis_skill(
720
+ zip_path: Path,
721
+ install_root: Path,
722
+ version: str,
723
+ expected_hash: str,
724
+ package_version: str,
725
+ replace: bool,
726
+ ) -> dict:
727
+ skills_root = install_root / "skills"
626
728
  destination = skills_root / "foggy-ai-analysis"
627
- marker = destination / ".foggy-onboarding-install.json"
628
- if destination.exists():
629
- if marker.is_file():
630
- installed = json.loads(marker.read_text(encoding="utf-8"))
631
- if installed.get("archiveSha256") == expected_hash:
632
- return {"path": str(destination), "version": version, "action": "kept-matching"}
633
- if not replace:
634
- raise OnboardingError(f"Analysis Skill already exists and is not managed at {destination}; rerun with --replace-skill to back it up")
635
- backup_root = project_root / ".foggy" / "onboarding-backups"
636
- backup_root.mkdir(parents=True, exist_ok=True)
637
- backup = backup_root / f"foggy-ai-analysis-{dt.datetime.now().strftime('%Y%m%d-%H%M%S')}"
638
- shutil.move(str(destination), str(backup))
639
- skills_root.mkdir(parents=True, exist_ok=True)
640
729
  with tempfile.TemporaryDirectory(prefix="foggy-skill-") as temporary:
641
730
  extract_root = Path(temporary)
642
731
  safe_extract(zip_path, extract_root)
@@ -644,21 +733,41 @@ def install_analysis_skill(zip_path: Path, project_root: Path, version: str, exp
644
733
  if len(candidates) != 1:
645
734
  raise OnboardingError(f"Expected exactly one SKILL.md in analysis Skill archive, found {len(candidates)}")
646
735
  source = candidates[0].parent
736
+ source_digest = skill_tree_digest(source)
737
+ if destination.exists():
738
+ installed = read_skill_marker(destination)
739
+ actual_digest = skill_tree_digest(destination)
740
+ marker_matches = bool(
741
+ installed
742
+ and installed.get("schemaVersion") in (MANAGED_SKILL_SCHEMA, "foggy-installed-skill/v1")
743
+ and installed.get("archiveSha256") == expected_hash
744
+ and (installed.get("sourceDigest") or installed.get("installedDigest") or actual_digest) == source_digest
745
+ and actual_digest == source_digest
746
+ )
747
+ if marker_matches:
748
+ marker = write_skill_marker(
749
+ destination,
750
+ kind="analysis",
751
+ package_version=package_version,
752
+ component_version=version,
753
+ source_digest=source_digest,
754
+ archive_sha256=expected_hash,
755
+ )
756
+ return {"path": str(destination), "version": version, "digest": marker["installedDigest"], "managed": True, "action": "kept-matching"}
757
+ if not replace:
758
+ raise OnboardingError(f"Analysis Skill is missing, modified, or outdated at {destination}; use the plugin Repair action to back it up and restore it")
759
+ backup_skill(destination, install_root / "skill-backups")
760
+ skills_root.mkdir(parents=True, exist_ok=True)
647
761
  shutil.copytree(source, destination)
648
- atomic_json(marker, {"schemaVersion": "foggy-installed-skill/v1", "version": version, "archiveSha256": expected_hash})
649
- return {"path": str(destination), "version": version, "action": "installed"}
650
-
651
-
652
- def install_onboarding_skill(project_root: Path) -> dict:
653
- destination = project_root / ".agents" / "skills" / "foggy-deepseek-onboarding"
654
- source = skill_root()
655
- if source.resolve() == destination.resolve(strict=False):
656
- return {"path": str(destination), "action": "already-running-from-target"}
657
- if destination.exists():
658
- return {"path": str(destination), "action": "kept-existing"}
659
- destination.parent.mkdir(parents=True, exist_ok=True)
660
- shutil.copytree(source, destination)
661
- return {"path": str(destination), "action": "installed"}
762
+ marker = write_skill_marker(
763
+ destination,
764
+ kind="analysis",
765
+ package_version=package_version,
766
+ component_version=version,
767
+ source_digest=source_digest,
768
+ archive_sha256=expected_hash,
769
+ )
770
+ return {"path": str(destination), "version": version, "digest": marker["installedDigest"], "managed": True, "action": "installed"}
662
771
 
663
772
 
664
773
  def read_install_state(install_root: Path, required: bool = True) -> dict | None:
@@ -687,7 +796,6 @@ def install_command(args: argparse.Namespace) -> dict:
687
796
  versions = load_versions()
688
797
  install_root = normalized(args.install_root or default_install_root())
689
798
  data_root = normalized(args.data_root or default_data_root())
690
- project_root = normalized(args.project_root or Path.cwd())
691
799
  cache_dirs = [normalized(item) for item in args.asset_cache_dir]
692
800
  components = versions["components"]
693
801
  assert_managed_root(install_root, "Install root")
@@ -698,9 +806,9 @@ def install_command(args: argparse.Namespace) -> dict:
698
806
  "schemaVersion": "foggy-deepseek-onboarding-plan/v1",
699
807
  "installRoot": str(install_root),
700
808
  "dataRoot": str(data_root),
701
- "projectRoot": str(project_root),
809
+ "workspaceMode": "dsh-session-cwd",
702
810
  "versions": {name: value.get("version") for name, value in components.items()},
703
- "operations": ["install isolated CLI", "verify Launcher assets", "install project Skills", "write install state"],
811
+ "operations": ["install isolated CLI", "verify Launcher assets", "install global analysis Skill", "write install state"],
704
812
  "productionReady": False,
705
813
  }
706
814
  if args.dry_run:
@@ -714,8 +822,6 @@ def install_command(args: argparse.Namespace) -> dict:
714
822
  progress.update("preflight", 0, "Checking prerequisites")
715
823
  if sys.version_info < (3, 11):
716
824
  raise OnboardingError(f"Python 3.11+ required, got {sys.version.split()[0]}")
717
- if not project_root.is_dir():
718
- raise OnboardingError(f"Project root not found: {project_root}")
719
825
  install_root.mkdir(parents=True, exist_ok=True)
720
826
  data_root.mkdir(parents=True, exist_ok=True)
721
827
  downloads = install_root / "downloads"
@@ -820,19 +926,26 @@ def install_command(args: argparse.Namespace) -> dict:
820
926
  zip_asset = next(item for item in analysis_assets if item["role"] == "zip")
821
927
  progress.update("analysis-skill", 3, "Installing analysis Skill", fraction=0.9, current_file=zip_asset["file"])
822
928
  analysis_skill = install_analysis_skill(
823
- downloads / "skill" / zip_asset["file"], project_root, components["analysisSkill"]["version"], zip_asset["sha256"], args.replace_skill
929
+ downloads / "skill" / zip_asset["file"], install_root, components["analysisSkill"]["version"],
930
+ zip_asset["sha256"], versions["packageVersion"], args.replace_skill,
824
931
  )
825
932
  progress.update("analysis-skill", 3, "Analysis Skill ready", fraction=1.0)
826
- progress.update("workspace-skills", 4, "Installing onboarding Skill", fraction=0.1)
827
- onboarding_skill = install_onboarding_skill(project_root)
828
- progress.update("workspace-skills", 4, "Workspace Skills ready", fraction=1.0)
933
+ progress.update("workspace-skills", 4, "Registering native DSH Skills", fraction=0.1)
934
+ onboarding_skill = {
935
+ "path": str(skill_root()),
936
+ "version": versions["packageVersion"],
937
+ "managed": False,
938
+ "action": "provided-by-plugin",
939
+ "provider": "foggy-managed-skills",
940
+ }
941
+ progress.update("workspace-skills", 4, "Native DSH Skills ready", fraction=1.0)
829
942
  state = {
830
943
  "schemaVersion": STATE_SCHEMA,
831
944
  "installedAt": now_utc(),
832
945
  "packageVersion": versions["packageVersion"],
833
946
  "installRoot": str(install_root),
834
947
  "dataRoot": str(data_root),
835
- "projectRoot": str(project_root),
948
+ "workspaceMode": "dsh-session-cwd",
836
949
  "cli": {"version": cli_component["version"], "command": str(cli_command), "mode": cli_mode},
837
950
  "launcher": {"version": components["launcher"]["version"], "path": str(launcher_dir)},
838
951
  "skills": {"onboarding": onboarding_skill, "analysis": analysis_skill},
@@ -850,7 +963,7 @@ def install_command(args: argparse.Namespace) -> dict:
850
963
  "statePath": str(install_root / "install-state.json"),
851
964
  "installRoot": str(install_root),
852
965
  "dataRoot": str(data_root),
853
- "projectRoot": str(project_root),
966
+ "workspaceMode": "dsh-session-cwd",
854
967
  "cliVersion": cli_component["version"],
855
968
  "launcherVersion": components["launcher"]["version"],
856
969
  "analysisSkill": analysis_skill,
@@ -1112,7 +1225,7 @@ def require_opaque_profile_cli(install_state: dict) -> None:
1112
1225
  def onboarding_plan_command(args: argparse.Namespace) -> dict:
1113
1226
  install_root, install_state, data_root, runtime_state = onboarding_context(args, require_runtime=False)
1114
1227
  profile = safe_profile(args.profile)
1115
- project_root = normalized(args.project_root or install_state["projectRoot"])
1228
+ project_root = normalized(args.project_root or Path.cwd())
1116
1229
  if not project_root.is_dir():
1117
1230
  raise OnboardingError(f"Project root not found: {project_root}")
1118
1231
  connection_file = normalized(args.connection_file)
@@ -1772,7 +1885,7 @@ def save_composite_result(evidence_dir: Path, name: str, payload: dict, files: l
1772
1885
 
1773
1886
  def datasource_run_command(args: argparse.Namespace) -> dict:
1774
1887
  _install_root, install_state, data_root, _runtime_state = onboarding_context(args, require_runtime=True)
1775
- project_root = normalized(args.project_root or install_state["projectRoot"])
1888
+ project_root = normalized(args.project_root or Path.cwd())
1776
1889
  requested_connection = validate_connection(read_json_object(normalized(args.connection_file), "Connection plan"))
1777
1890
  if not requested_connection.get("profile"):
1778
1891
  raise OnboardingError("Composite datasource onboarding requires connection.profile in the approved contract")
@@ -2032,8 +2145,17 @@ def doctor_command(args: argparse.Namespace) -> dict:
2032
2145
  path = launcher_dir / asset["file"]
2033
2146
  launcher_checks.append({"file": asset["file"], "present": path.is_file(), "sha256Valid": path.is_file() and sha256(path) == asset["sha256"]})
2034
2147
  launcher_ok = bool(launcher_checks) and all(item["present"] and item["sha256Valid"] for item in launcher_checks)
2035
- analysis_skill = project_root / ".agents" / "skills" / "foggy-ai-analysis" / "SKILL.md"
2036
- onboarding_skill = project_root / ".agents" / "skills" / "foggy-deepseek-onboarding" / "SKILL.md"
2148
+ analysis_skill_root = normalized(state.get("skills", {}).get("analysis", {}).get("path") or install_root / "skills" / "foggy-ai-analysis") if state else install_root / "skills" / "foggy-ai-analysis"
2149
+ onboarding_skill_root = skill_root()
2150
+ analysis_skill = managed_skill_status(analysis_skill_root, "analysis", versions["components"]["analysisSkill"]["version"])
2151
+ onboarding_skill = {
2152
+ "path": str(onboarding_skill_root),
2153
+ "present": (onboarding_skill_root / "SKILL.md").is_file(),
2154
+ "managed": False,
2155
+ "version": versions["packageVersion"],
2156
+ "provider": "foggy-managed-skills",
2157
+ "valid": (onboarding_skill_root / "SKILL.md").is_file(),
2158
+ }
2037
2159
  runtime = {"status": "stopped"}
2038
2160
  if state:
2039
2161
  data_root = normalized(state["dataRoot"])
@@ -2047,7 +2169,8 @@ def doctor_command(args: argparse.Namespace) -> dict:
2047
2169
  "java": java_ok,
2048
2170
  "cli": cli_ok,
2049
2171
  "launcher": launcher_ok,
2050
- "analysisSkill": analysis_skill.is_file(),
2172
+ "analysisSkill": analysis_skill["valid"],
2173
+ "onboardingSkill": onboarding_skill["valid"],
2051
2174
  }
2052
2175
  if args.strict_runtime:
2053
2176
  required["runtime"] = runtime["status"] == "running"
@@ -2061,7 +2184,8 @@ def doctor_command(args: argparse.Namespace) -> dict:
2061
2184
  "java": java,
2062
2185
  "cli": cli,
2063
2186
  "launcherAssets": launcher_checks,
2064
- "skills": {"analysis": str(analysis_skill), "onboarding": str(onboarding_skill), "onboardingPresent": onboarding_skill.is_file()},
2187
+ "skills": {"analysis": analysis_skill, "onboarding": onboarding_skill},
2188
+ "workspace": {"path": str(project_root), "mode": "dsh-session-cwd"},
2065
2189
  "runtime": runtime,
2066
2190
  "environmentPresence": {name: bool(os.environ.get(name)) for name in ("DEEPSEEK_API_KEY", "ALIYUN_TOKEN_PLAN_API_KEY", "FOGGY_RUNTIME_API_AUTH_CODE", "FOGGY_RUNTIME_AUTHORIZATION")},
2067
2191
  "productionReady": False,
@@ -2077,13 +2201,8 @@ def uninstall_command(args: argparse.Namespace) -> dict:
2077
2201
  assert_managed_root(data_root, "Data root")
2078
2202
  if install_root == data_root:
2079
2203
  raise OnboardingError("Install root and data root must be different")
2080
- project_root = normalized(state["projectRoot"])
2081
- skills_root = project_root / ".agents" / "skills"
2082
- skill_targets = [skills_root / name for name in ("foggy-deepseek-onboarding", "foggy-ai-analysis")]
2083
- if args.remove_skills:
2084
- for target in skill_targets:
2085
- if target.exists() and (target.is_symlink() or not is_child(target, skills_root)):
2086
- raise OnboardingError(f"Refusing to remove unexpected Skill path: {target}")
2204
+ skills_root = install_root / "skills"
2205
+ skill_targets = [skills_root / "foggy-ai-analysis"]
2087
2206
  plan = {"installRoot": str(install_root), "dataRoot": str(data_root), "removeSkills": args.remove_skills, "purgeData": args.purge_data}
2088
2207
  if args.dry_run:
2089
2208
  return {"success": True, "dryRun": True, "plan": plan}