@foggy-projects/deepseek-harness-plugin 0.4.0-beta.2 → 0.4.0-beta.4

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.2.tgz
11
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.4.tgz
12
12
  ```
13
13
 
14
14
  Restart `dsh web`, open Settings → Plugins → Foggy Data Analysis, and initialize
@@ -27,6 +27,21 @@ asset-cache directories. Neither variable is required for a normal install.
27
27
  Database credentials are deliberately outside the ordinary DSH settings
28
28
  document. The database and semantic-layer wizard is the next Bundle milestone.
29
29
 
30
+ ## Managed workspace contract
31
+
32
+ Initialization writes a non-secret discovery document at
33
+ `.foggy/deepseek-harness/context.json` in the selected project. It points agents
34
+ to the authoritative global install state, the managed CLI's absolute command,
35
+ the Runtime state, and the two workspace Skills. The managed CLI is intentionally
36
+ isolated and does not need to be on `PATH`.
37
+
38
+ Both `foggy-deepseek-onboarding` and `foggy-ai-analysis` carry a
39
+ `.foggy-managed-skill.json` marker. **Re-download / Repair** verifies their
40
+ managed content, backs up a modified or outdated Skill under
41
+ `.foggy/onboarding-backups`, restores missing content, and regenerates the
42
+ project context. If a Skill is restored while Harness is already running, start
43
+ a new task or restart Harness so its Skill registry can reload it.
44
+
30
45
  ## Linux and WSL2 experience
31
46
 
32
47
  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.2"
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
+ projectContext: '项目安装上下文',
44
46
  python: 'Python',
45
47
  java: 'Java',
46
48
  installed: '已安装',
@@ -52,6 +54,15 @@ window.__ModuleLoader__.load({
52
54
  start: '启动 Runtime',
53
55
  stop: '停止 Runtime',
54
56
  working: '操作进行中…',
57
+ progressWorking: '正在初始化 Foggy…',
58
+ progressPreflight: '检查运行环境',
59
+ progressCli: '准备 CLI',
60
+ progressLauncher: '准备 Launcher',
61
+ progressAnalysis: '准备分析 Skill',
62
+ progressSkills: '安装工作区 Skills',
63
+ progressState: '写入安装状态',
64
+ progressComplete: '初始化完成',
65
+ progressFiles: '文件',
55
66
  accepted: '操作已开始;页面会自动刷新状态。',
56
67
  actionFailed: '操作失败',
57
68
  roots: '本地目录',
@@ -59,8 +70,9 @@ window.__ModuleLoader__.load({
59
70
  dataRoot: '数据目录',
60
71
  runtimeUrl: 'Runtime 地址',
61
72
  projectRoot: 'Skill 目标工作区',
73
+ contextPath: '对话发现文件',
62
74
  nextTitle: '后续配置',
63
- nextCopy: 'Runtime 启动成功后,将继续进入数据库连接与语义层向导。数据库密码不会写入 DSH 普通设置。',
75
+ nextCopy: '对话会从项目内固定的 context.json 读取 CLI、Runtime Skill 安装状态,不再依赖 PATH 猜测。Runtime 启动成功后,将继续进入数据库连接与语义层向导。',
64
76
  beta: 'Beta',
65
77
  }
66
78
 
@@ -79,6 +91,8 @@ window.__ModuleLoader__.load({
79
91
  cli: 'CLI',
80
92
  launcher: 'Launcher',
81
93
  analysisSkill: 'Analysis Skill',
94
+ onboardingSkill: 'Onboarding Skill',
95
+ projectContext: 'Project install context',
82
96
  python: 'Python',
83
97
  java: 'Java',
84
98
  installed: 'Installed',
@@ -90,6 +104,15 @@ window.__ModuleLoader__.load({
90
104
  start: 'Start Runtime',
91
105
  stop: 'Stop Runtime',
92
106
  working: 'Operation in progress…',
107
+ progressWorking: 'Initializing Foggy…',
108
+ progressPreflight: 'Checking prerequisites',
109
+ progressCli: 'Preparing CLI',
110
+ progressLauncher: 'Preparing Launcher',
111
+ progressAnalysis: 'Preparing analysis Skill',
112
+ progressSkills: 'Installing workspace Skills',
113
+ progressState: 'Writing install state',
114
+ progressComplete: 'Initialization complete',
115
+ progressFiles: 'files',
93
116
  accepted: 'The operation started; this page will refresh automatically.',
94
117
  actionFailed: 'Operation failed',
95
118
  roots: 'Local directories',
@@ -97,8 +120,9 @@ window.__ModuleLoader__.load({
97
120
  dataRoot: 'Data',
98
121
  runtimeUrl: 'Runtime URL',
99
122
  projectRoot: 'Skill target workspace',
123
+ contextPath: 'Conversation discovery file',
100
124
  nextTitle: 'Next configuration',
101
- nextCopy: 'After the Runtime starts, the database connection and semantic-layer wizard comes next. Database passwords are never stored in ordinary DSH settings.',
125
+ nextCopy: 'Conversations discover the installed CLI, Runtime, and Skills from the fixed project context.json instead of guessing from PATH. After Runtime starts, the database connection and semantic-layer wizard comes next.',
102
126
  beta: 'Beta',
103
127
  }
104
128
 
@@ -110,6 +134,7 @@ window.__ModuleLoader__.load({
110
134
  .foggy-state{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600}.foggy-dot{width:9px;height:9px;border-radius:50%;background:var(--dsw-alias-label-tertiary)}.foggy-dot[data-state=ready],.foggy-dot[data-state=running]{background:var(--dsw-alias-state-success-primary)}.foggy-dot[data-state=degraded]{background:var(--dsw-alias-state-error-primary)}
111
135
  .foggy-actions{display:flex;gap:8px;flex-wrap:wrap}.foggy-button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:7px 12px;cursor:pointer}.foggy-button:hover{background:var(--dsw-alias-interactive-bg-hover)}.foggy-button:disabled{opacity:.55;cursor:not-allowed}.foggy-button-primary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:white}
112
136
  .foggy-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.foggy-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-card-head{display:flex;justify-content:space-between;gap:8px}.foggy-card strong{font-size:13px}.foggy-badge{font-size:11px;color:var(--dsw-alias-label-secondary)}.foggy-card code{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;overflow-wrap:anywhere}
137
+ .foggy-progress{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-progress-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px}.foggy-progress-head strong{font-weight:600}.foggy-progress-percent{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}.foggy-progress-track{height:8px;margin-top:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.foggy-progress-fill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.foggy-progress-meta{display:flex;justify-content:space-between;gap:12px;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.foggy-progress-file{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
113
138
  .foggy-message{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.foggy-message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.foggy-paths,.foggy-next{border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.foggy-paths h4,.foggy-next h4{margin:0 0 8px;font-size:13px}.foggy-paths dl{display:grid;grid-template-columns:100px minmax(0,1fr);gap:6px 10px;margin:0}.foggy-paths dt{color:var(--dsw-alias-label-tertiary);font-size:12px}.foggy-paths dd{margin:0;min-width:0;overflow-wrap:anywhere;font-family:var(--ds-font-family-code);font-size:11px}.foggy-next p{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
114
139
  @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}}
115
140
  `
@@ -146,6 +171,52 @@ window.__ModuleLoader__.load({
146
171
  })
147
172
  }
148
173
 
174
+ function progressPanel(progress, t) {
175
+ if (!progress) return null
176
+ const percent = Math.max(0, Math.min(100, Number(progress.percent) || 0))
177
+ const phaseKeys = {
178
+ preflight: 'progressPreflight',
179
+ cli: 'progressCli',
180
+ launcher: 'progressLauncher',
181
+ 'analysis-skill': 'progressAnalysis',
182
+ 'workspace-skills': 'progressSkills',
183
+ state: 'progressState',
184
+ complete: 'progressComplete',
185
+ }
186
+ const phase = t(phaseKeys[progress.phase] || 'progressWorking')
187
+ const step = progress.step?.index && progress.step?.total
188
+ ? `${progress.step.index}/${progress.step.total}`
189
+ : ''
190
+ const files = progress.files?.total
191
+ ? `${progress.files.completed || 0}/${progress.files.total} ${t('progressFiles')}`
192
+ : ''
193
+ return jsxs('div', {
194
+ className: 'foggy-progress',
195
+ role: 'status',
196
+ 'aria-live': 'polite',
197
+ children: [
198
+ jsxs('div', { className: 'foggy-progress-head', children: [
199
+ jsx('strong', { children: progress.message || phase }),
200
+ jsx('span', { className: 'foggy-progress-percent', children: `${percent}%` }),
201
+ ] }),
202
+ jsx('div', {
203
+ className: 'foggy-progress-track',
204
+ role: 'progressbar',
205
+ 'aria-valuemin': 0,
206
+ 'aria-valuemax': 100,
207
+ 'aria-valuenow': percent,
208
+ 'aria-label': phase,
209
+ children: jsx('div', { className: 'foggy-progress-fill', style: { width: `${percent}%` } }),
210
+ }),
211
+ jsxs('div', { className: 'foggy-progress-meta', children: [
212
+ jsx('span', { children: phase }),
213
+ jsx('span', { children: [step, files].filter(Boolean).join(' · ') }),
214
+ ] }),
215
+ progress.currentFile ? jsx('div', { className: 'foggy-progress-meta foggy-progress-file', title: progress.currentFile, children: progress.currentFile }) : null,
216
+ ],
217
+ })
218
+ }
219
+
149
220
  function FoggySettingsTab({ api, t }) {
150
221
  const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false })
151
222
 
@@ -188,6 +259,7 @@ window.__ModuleLoader__.load({
188
259
  const operationError = status.operation?.state === 'failed'
189
260
  ? status.operation.error || status.operation.result?.error?.message || t('actionFailed')
190
261
  : ''
262
+ const progress = busy ? progressPanel(status.operation?.progress, t) : null
191
263
  const stateLabels = { 'not-installed': 'notInstalled', degraded: 'degraded', ready: 'ready', running: 'running' }
192
264
  return jsxs('section', { className: 'foggy-settings', children: [
193
265
  jsxs('div', { className: 'foggy-hero', children: [
@@ -198,12 +270,13 @@ window.__ModuleLoader__.load({
198
270
  jsxs('div', { className: 'foggy-state', children: [jsx('span', { className: 'foggy-dot', 'data-state': status.state }), jsx('span', { children: t(stateLabels[status.state] || 'notInstalled') })] }),
199
271
  jsxs('div', { className: 'foggy-actions', children: [
200
272
  jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: refresh, children: t('refresh') }),
201
- !status.installed ? jsx('button', { className: 'foggy-button foggy-button-primary', type: 'button', disabled: busy, onClick: () => run('initialize'), children: t('initialize') }) : null,
202
- status.installed ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('repair'), children: t('repair') }) : null,
273
+ status.state === 'not-installed' ? jsx('button', { className: 'foggy-button foggy-button-primary', type: 'button', disabled: busy, onClick: () => run('initialize'), children: t('initialize') }) : null,
274
+ status.state !== 'not-installed' ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('repair'), children: t('repair') }) : null,
203
275
  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,
204
276
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
205
277
  ] }),
206
278
  ] }),
279
+ progress,
207
280
  busy || operationError || view.message ? jsx('p', { className: 'foggy-message', 'data-error': operationError || view.error ? 'true' : undefined, children: busy ? t('working') : operationError || view.message }) : null,
208
281
  jsxs('div', { className: 'foggy-grid', children: [
209
282
  componentCard(t('python'), status.components.python, 'tool', t),
@@ -211,6 +284,8 @@ window.__ModuleLoader__.load({
211
284
  componentCard(t('cli'), status.components.cli, 'runtime', t),
212
285
  componentCard(t('launcher'), status.components.launcher, 'runtime', t),
213
286
  componentCard(t('analysisSkill'), status.components.analysisSkill, 'runtime', t),
287
+ componentCard(t('onboardingSkill'), status.components.onboardingSkill, 'runtime', t),
288
+ componentCard(t('projectContext'), status.components.projectContext, 'runtime', t),
214
289
  ] }),
215
290
  jsxs('div', { className: 'foggy-paths', children: [
216
291
  jsx('h4', { children: t('roots') }),
@@ -218,6 +293,7 @@ window.__ModuleLoader__.load({
218
293
  jsx('dt', { children: t('installRoot') }), jsx('dd', { children: status.roots.installRoot }),
219
294
  jsx('dt', { children: t('dataRoot') }), jsx('dd', { children: status.roots.dataRoot }),
220
295
  jsx('dt', { children: t('projectRoot') }), jsx('dd', { children: status.projectRoot }),
296
+ jsx('dt', { children: t('contextPath') }), jsx('dd', { children: status.contextPath }),
221
297
  status.runtimeUrl ? jsx('dt', { children: t('runtimeUrl') }) : null,
222
298
  status.runtimeUrl ? jsx('dd', { children: status.runtimeUrl }) : null,
223
299
  ] }),
package/lib/index.js CHANGED
@@ -46,6 +46,14 @@ async function readJson(path) {
46
46
  return JSON.parse(await readFile(path, 'utf8'))
47
47
  }
48
48
 
49
+ async function readOptionalJson(path) {
50
+ try {
51
+ return await readJson(path)
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+
49
57
  async function commandVersion(command, args) {
50
58
  try {
51
59
  const { stdout, stderr } = await execFileAsync(command, args, {
@@ -121,8 +129,11 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
121
129
 
122
130
  async status() {
123
131
  const roots = defaultRoots()
132
+ const projectRoot = process.env.FOGGY_PROJECT_ROOT || process.cwd()
124
133
  const statePath = join(roots.installRoot, 'install-state.json')
125
134
  const runtimeStatePath = join(roots.dataRoot, 'runtime-state.json')
135
+ const progressPath = join(roots.dataRoot, 'operation-progress.json')
136
+ const contextPath = join(projectRoot, '.foggy', 'deepseek-harness', 'context.json')
126
137
  const manifest = await readJson(versionsFile)
127
138
  let pythonProbe
128
139
  try {
@@ -134,10 +145,16 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
134
145
  const java = compatible(await commandVersion(process.env.JAVA_EXE || 'java', ['-version']), '17.0')
135
146
  let state = null
136
147
  let runtime = null
148
+ let context = null
137
149
  try { state = await readJson(statePath) } catch {}
138
150
  try { runtime = await readJson(runtimeStatePath) } catch {}
151
+ try { context = await readJson(contextPath) } catch {}
139
152
  const cliPath = state?.cli?.command
140
153
  const launcherPath = state?.launcher?.path
154
+ const analysisSkillPath = state?.skills?.analysis?.path || join(projectRoot, '.agents', 'skills', 'foggy-ai-analysis')
155
+ const onboardingSkillPath = state?.skills?.onboarding?.path || join(projectRoot, '.agents', 'skills', 'foggy-deepseek-onboarding')
156
+ const analysisMarker = await readOptionalJson(join(analysisSkillPath, '.foggy-managed-skill.json'))
157
+ const onboardingMarker = await readOptionalJson(join(onboardingSkillPath, '.foggy-managed-skill.json'))
141
158
  const components = {
142
159
  python,
143
160
  java,
@@ -150,12 +167,53 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
150
167
  version: state?.launcher?.version ?? manifest.components.launcher.version,
151
168
  },
152
169
  analysisSkill: {
153
- installed: Boolean(state?.skills?.analysis?.path && await exists(state.skills.analysis.path)),
170
+ installed: Boolean(
171
+ await exists(join(analysisSkillPath, 'SKILL.md'))
172
+ && analysisMarker?.schemaVersion === 'foggy-managed-skill/v1'
173
+ && analysisMarker?.componentVersion === manifest.components.analysisSkill.version
174
+ ),
154
175
  version: state?.skills?.analysis?.version ?? manifest.components.analysisSkill.version,
155
176
  },
177
+ onboardingSkill: {
178
+ installed: Boolean(
179
+ await exists(join(onboardingSkillPath, 'SKILL.md'))
180
+ && onboardingMarker?.schemaVersion === 'foggy-managed-skill/v1'
181
+ && onboardingMarker?.packageVersion === manifest.packageVersion
182
+ ),
183
+ version: state?.skills?.onboarding?.version ?? state?.packageVersion ?? manifest.packageVersion,
184
+ },
185
+ projectContext: {
186
+ installed: Boolean(
187
+ context?.schemaVersion === 'foggy-deepseek-harness-context/v1'
188
+ && context?.packageVersion === manifest.packageVersion
189
+ && context?.projectRoot === projectRoot
190
+ && context?.installStatePath === statePath
191
+ ),
192
+ version: context?.schemaVersion ?? 'foggy-deepseek-harness-context/v1',
193
+ },
156
194
  }
157
- const installed = components.cli.installed && components.launcher.installed && components.analysisSkill.installed
195
+ const installed = components.cli.installed
196
+ && components.launcher.installed
197
+ && components.analysisSkill.installed
198
+ && components.onboardingSkill.installed
199
+ && components.projectContext.installed
158
200
  const running = Boolean(runtime && processRunning(Number(runtime.pid)))
201
+ const progress = await readOptionalJson(progressPath)
202
+ let operation = operationView(this.operation)
203
+ if (operation.state === 'running' && progress?.operationId === operation.id) {
204
+ operation = { ...operation, progress }
205
+ } else if (!this.operation && progress?.state === 'running' && progress.operationId) {
206
+ operation = {
207
+ id: progress.operationId,
208
+ kind: progress.kind || 'initialize',
209
+ state: 'running',
210
+ startedAt: progress.startedAt || null,
211
+ finishedAt: null,
212
+ result: null,
213
+ error: null,
214
+ progress,
215
+ }
216
+ }
159
217
  return {
160
218
  success: true,
161
219
  packageVersion: manifest.packageVersion,
@@ -164,9 +222,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
164
222
  running,
165
223
  runtimeUrl: running ? runtime.runtimeUrl ?? null : null,
166
224
  roots,
167
- projectRoot: process.env.FOGGY_PROJECT_ROOT || process.cwd(),
225
+ projectRoot,
226
+ contextPath,
168
227
  components,
169
- operation: operationView(this.operation),
228
+ operation,
170
229
  next: installed ? (running ? 'configure-database' : 'start-runtime') : 'initialize',
171
230
  }
172
231
  }
@@ -183,7 +242,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
183
242
  'create isolated Python environment',
184
243
  'download and verify pinned CLI and Launcher assets',
185
244
  'install Foggy analysis and onboarding Skills into the workspace',
186
- 'write local install state',
245
+ 'write global install state and project-relative discovery context',
187
246
  ],
188
247
  secretsInDshSettings: false,
189
248
  }
@@ -211,6 +270,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
211
270
  }
212
271
  const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
213
272
  const args = explicitArgs ?? ['install', '--project-root', process.env.FOGGY_PROJECT_ROOT || process.cwd()]
273
+ if (!explicitArgs) {
274
+ const roots = defaultRoots()
275
+ args.push('--progress-file', join(roots.dataRoot, 'operation-progress.json'), '--operation-id', id, '--operation-kind', kind)
276
+ }
214
277
  if (replaceSkill) args.push('--replace-skill')
215
278
  for (const cacheDir of (process.env.FOGGY_ASSET_CACHE_DIRS || '').split(delimiter).filter(Boolean)) {
216
279
  args.push('--asset-cache-dir', cacheDir)
@@ -223,6 +286,17 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
223
286
  finishedAt: null,
224
287
  result: null,
225
288
  error: null,
289
+ progress: !explicitArgs ? {
290
+ schemaVersion: 'foggy-deepseek-onboarding-progress/v1',
291
+ operationId: id,
292
+ kind,
293
+ state: 'running',
294
+ phase: 'preflight',
295
+ message: 'Starting Foggy initialization',
296
+ currentFile: null,
297
+ percent: 0,
298
+ step: { index: 1, total: 6 },
299
+ } : null,
226
300
  promise: null,
227
301
  }
228
302
  operation.promise = runOnboarding(args)
@@ -230,11 +304,27 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
230
304
  operation.state = result.success === false ? 'failed' : 'succeeded'
231
305
  operation.result = result
232
306
  operation.finishedAt = new Date().toISOString()
307
+ if (operation.progress) {
308
+ operation.progress = {
309
+ ...operation.progress,
310
+ state: operation.state,
311
+ phase: operation.state === 'succeeded' ? 'complete' : operation.progress.phase,
312
+ message: operation.state === 'succeeded' ? 'Foggy initialization completed' : 'Foggy initialization failed',
313
+ percent: operation.state === 'succeeded' ? 100 : operation.progress.percent,
314
+ }
315
+ }
233
316
  })
234
317
  .catch((error) => {
235
318
  operation.state = 'failed'
236
319
  operation.error = String(error.message ?? error)
237
320
  operation.finishedAt = new Date().toISOString()
321
+ if (operation.progress) {
322
+ operation.progress = {
323
+ ...operation.progress,
324
+ state: 'failed',
325
+ message: 'Foggy initialization failed',
326
+ }
327
+ }
238
328
  })
239
329
  this.operation = operation
240
330
  return { success: true, accepted: true, operation: operationView(operation) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foggy-projects/deepseek-harness-plugin",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0-beta.4",
4
4
  "description": "Foggy Java data analysis engine integration for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -7,6 +7,23 @@ 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
+ ## Installed-context gate
11
+
12
+ Before deciding that Foggy, its CLI, or either Skill is missing, look for the project-relative file
13
+ `.foggy/deepseek-harness/context.json` from the current workspace root:
14
+
15
+ - Treat a valid `foggy-deepseek-harness-context/v1` document as the discovery pointer to the global
16
+ install state, absolute managed CLI command, Runtime state, and both installed Skills.
17
+ - Confirm the installation with this Skill's `doctor` wrapper. The absence of `foggy-runtime` from
18
+ `PATH` is not evidence that the managed CLI is missing; the plugin intentionally installs it in an
19
+ isolated environment and records its absolute command in the context and install-state files.
20
+ - Do not independently download or reinstall the CLI when valid project context exists. If the
21
+ context, a managed marker, or a Skill is missing or invalid, ask the user to open the Foggy plugin
22
+ settings and use **Re-download / Repair**. That operation regenerates context and restores managed
23
+ Skills, preserving a backup of modified or outdated Skill directories.
24
+ - If the plugin reports ready but this Skill was restored during the current Harness task, tell the
25
+ user that a new task or Harness restart may be needed for the Skill registry to reload.
26
+
10
27
  ## Mandatory orchestration boundary
11
28
 
12
29
  For every new-database onboarding session, this Skill is the orchestration authority until
@@ -42,10 +59,11 @@ For every new-database onboarding session, this Skill is the orchestration autho
42
59
 
43
60
  ## Workflow
44
61
 
45
- 1. Run `scripts/doctor.ps1 --project-root <root>` on Windows or
62
+ 1. Read `.foggy/deepseek-harness/context.json`, then run `scripts/doctor.ps1 --project-root <root>` on Windows or
46
63
  `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.
64
+ 2. If the pinned CLI, Launcher, project context, or either managed Skill is missing, use the Foggy
65
+ plugin's Repair action. Use the matching install script only when the plugin UI is unavailable;
66
+ use `--dry-run` first when paths or permissions are uncertain.
49
67
  3. Run `runtime-start` and require successful `wait-ready` plus `capabilities`. Record engine,
50
68
  Runtime API version, schema version, security mode, URL, namespace, PID, and evidence path.
51
69
  4. Confirm the project contains `.agents/skills/foggy-ai-analysis/SKILL.md`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "foggy-deepseek-onboarding-versions/v1",
3
- "packageVersion": "0.4.0-beta.2",
3
+ "packageVersion": "0.4.0-beta.4",
4
4
  "validatedAt": "2026-08-28",
5
5
  "components": {
6
6
  "deepseekHarness": {
@@ -25,6 +25,10 @@ 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
+ CONTEXT_SCHEMA = "foggy-deepseek-harness-context/v1"
29
+ MANAGED_SKILL_SCHEMA = "foggy-managed-skill/v1"
30
+ MANAGED_SKILL_MARKER = ".foggy-managed-skill.json"
31
+ LEGACY_SKILL_MARKER = ".foggy-onboarding-install.json"
28
32
  CONNECTION_SCHEMA = "foggy-deepseek-connection/v1"
29
33
  SEMANTIC_PLAN_SCHEMA = "foggy-deepseek-semantic-plan/v1"
30
34
  PROFILE_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]{0,62}$")
@@ -38,6 +42,93 @@ class OnboardingError(RuntimeError):
38
42
  pass
39
43
 
40
44
 
45
+ PROGRESS_SCHEMA = "foggy-deepseek-onboarding-progress/v1"
46
+ ACTIVE_PROGRESS: "ProgressReporter | None" = None
47
+
48
+
49
+ class ProgressReporter:
50
+ total_steps = 6
51
+
52
+ def __init__(self, path: str | None, operation_id: str | None, kind: str) -> None:
53
+ self.path = Path(path).expanduser() if path else None
54
+ self.operation_id = operation_id
55
+ self.kind = kind
56
+ self.started_at = now_utc()
57
+ self.phase = "preflight"
58
+ self.step_index = 0
59
+ self.fraction = 0.0
60
+ self.message = ""
61
+ self.current_file: str | None = None
62
+ self.completed_files: int | None = None
63
+ self.total_files: int | None = None
64
+
65
+ @property
66
+ def enabled(self) -> bool:
67
+ return self.path is not None
68
+
69
+ def _write(self, state: str, *, finished_at: str | None = None, error: str | None = None) -> None:
70
+ if not self.path:
71
+ return
72
+ bounded_step = max(0, min(self.step_index, self.total_steps))
73
+ bounded_fraction = max(0.0, min(float(self.fraction), 1.0))
74
+ percent = 100 if state == "succeeded" else round(((bounded_step + bounded_fraction) / self.total_steps) * 100)
75
+ payload = {
76
+ "schemaVersion": PROGRESS_SCHEMA,
77
+ "operationId": self.operation_id,
78
+ "kind": self.kind,
79
+ "state": state,
80
+ "phase": self.phase,
81
+ "message": self.message,
82
+ "currentFile": self.current_file,
83
+ "percent": percent,
84
+ "step": {"index": min(bounded_step + 1, self.total_steps), "total": self.total_steps},
85
+ "startedAt": self.started_at,
86
+ "updatedAt": now_utc(),
87
+ }
88
+ if self.completed_files is not None and self.total_files is not None:
89
+ payload["files"] = {"completed": self.completed_files, "total": self.total_files}
90
+ if finished_at:
91
+ payload["finishedAt"] = finished_at
92
+ if error:
93
+ payload["error"] = error
94
+ self.path.parent.mkdir(parents=True, exist_ok=True)
95
+ atomic_json(self.path, payload)
96
+
97
+ def update(
98
+ self,
99
+ phase: str,
100
+ step_index: int,
101
+ message: str,
102
+ *,
103
+ fraction: float = 0.0,
104
+ current_file: str | None = None,
105
+ completed_files: int | None = None,
106
+ total_files: int | None = None,
107
+ ) -> None:
108
+ self.phase = phase
109
+ self.step_index = step_index
110
+ self.fraction = fraction
111
+ self.message = message
112
+ self.current_file = current_file
113
+ self.completed_files = completed_files
114
+ self.total_files = total_files
115
+ self._write("running")
116
+
117
+ def finish(self) -> None:
118
+ self.phase = "complete"
119
+ self.step_index = self.total_steps
120
+ self.fraction = 0.0
121
+ self.message = "Foggy initialization completed"
122
+ self.current_file = None
123
+ self.completed_files = None
124
+ self.total_files = None
125
+ self._write("succeeded", finished_at=now_utc())
126
+
127
+ def fail(self, error: object) -> None:
128
+ self.message = "Foggy initialization failed"
129
+ self._write("failed", finished_at=now_utc(), error=str(error))
130
+
131
+
41
132
  def now_utc() -> str:
42
133
  return dt.datetime.now(dt.timezone.utc).isoformat()
43
134
 
@@ -182,7 +273,18 @@ def cached_asset(name: str, expected: str, cache_dirs: list[Path]) -> Path | Non
182
273
  return None
183
274
 
184
275
 
185
- def materialize(asset: dict, destination: Path, cache_dirs: list[Path]) -> dict:
276
+ def materialize(
277
+ asset: dict,
278
+ destination: Path,
279
+ cache_dirs: list[Path],
280
+ *,
281
+ progress: ProgressReporter | None = None,
282
+ progress_phase: str = "",
283
+ progress_step: int = 0,
284
+ progress_index: int = 0,
285
+ progress_total: int = 1,
286
+ progress_message: str = "Downloading and verifying asset",
287
+ ) -> dict:
186
288
  destination.parent.mkdir(parents=True, exist_ok=True)
187
289
  if destination.is_file():
188
290
  verify_asset(destination, asset["sha256"])
@@ -196,7 +298,18 @@ def materialize(asset: dict, destination: Path, cache_dirs: list[Path]) -> dict:
196
298
  if temporary.exists():
197
299
  temporary.unlink()
198
300
  try:
199
- urllib.request.urlretrieve(asset["url"], temporary)
301
+ def reporthook(block_count: int, block_size: int, total_size: int) -> None:
302
+ if progress is None or not progress_phase or total_size <= 0:
303
+ return
304
+ downloaded = min(block_count * block_size, total_size)
305
+ fraction = (progress_index + downloaded / total_size) / max(progress_total, 1)
306
+ progress.update(
307
+ progress_phase, progress_step, progress_message,
308
+ fraction=fraction, current_file=asset["file"],
309
+ completed_files=progress_index, total_files=progress_total,
310
+ )
311
+
312
+ urllib.request.urlretrieve(asset["url"], temporary, reporthook=reporthook)
200
313
  verify_asset(temporary, asset["sha256"])
201
314
  os.replace(temporary, destination)
202
315
  finally:
@@ -222,6 +335,145 @@ def atomic_json(path: Path, payload: dict) -> None:
222
335
  os.replace(temporary, path)
223
336
 
224
337
 
338
+ def skill_tree_digest(root: Path) -> str:
339
+ """Hash managed Skill content while ignoring generated and management files."""
340
+ digest = hashlib.sha256()
341
+ ignored_names = {MANAGED_SKILL_MARKER, LEGACY_SKILL_MARKER}
342
+ files = sorted(
343
+ path for path in root.rglob("*")
344
+ if path.is_file()
345
+ and path.name not in ignored_names
346
+ and "__pycache__" not in path.parts
347
+ and path.suffix != ".pyc"
348
+ )
349
+ for path in files:
350
+ relative = path.relative_to(root).as_posix().encode("utf-8")
351
+ digest.update(len(relative).to_bytes(4, "big"))
352
+ digest.update(relative)
353
+ with path.open("rb") as stream:
354
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
355
+ digest.update(chunk)
356
+ return digest.hexdigest()
357
+
358
+
359
+ def read_skill_marker(destination: Path) -> dict | None:
360
+ for name in (MANAGED_SKILL_MARKER, LEGACY_SKILL_MARKER):
361
+ marker = destination / name
362
+ if marker.is_file():
363
+ try:
364
+ payload = json.loads(marker.read_text(encoding="utf-8"))
365
+ if isinstance(payload, dict):
366
+ return payload
367
+ except (OSError, json.JSONDecodeError):
368
+ return None
369
+ return None
370
+
371
+
372
+ def managed_skill_status(destination: Path, kind: str, expected_version: str) -> dict:
373
+ marker = read_skill_marker(destination)
374
+ present = (destination / "SKILL.md").is_file()
375
+ actual_digest = skill_tree_digest(destination) if destination.is_dir() else None
376
+ marker_digest = marker.get("installedDigest") if marker else None
377
+ managed = bool(
378
+ marker
379
+ and marker.get("schemaVersion") == MANAGED_SKILL_SCHEMA
380
+ and marker.get("kind") == kind
381
+ )
382
+ version = marker.get("componentVersion") if marker else None
383
+ return {
384
+ "path": str(destination),
385
+ "present": present,
386
+ "markerPresent": bool(marker),
387
+ "managed": managed,
388
+ "version": version,
389
+ "versionMatches": version == expected_version,
390
+ "integrityValid": bool(marker_digest and actual_digest == marker_digest),
391
+ "valid": bool(present and managed and version == expected_version and marker_digest and actual_digest == marker_digest),
392
+ }
393
+
394
+
395
+ def write_skill_marker(
396
+ destination: Path,
397
+ *,
398
+ kind: str,
399
+ package_version: str,
400
+ component_version: str,
401
+ source_digest: str,
402
+ archive_sha256: str | None = None,
403
+ ) -> dict:
404
+ marker = {
405
+ "schemaVersion": MANAGED_SKILL_SCHEMA,
406
+ "kind": kind,
407
+ "managedBy": "@foggy-projects/deepseek-harness-plugin",
408
+ "packageVersion": package_version,
409
+ "componentVersion": component_version,
410
+ "sourceDigest": source_digest,
411
+ "installedDigest": skill_tree_digest(destination),
412
+ "installedAt": now_utc(),
413
+ }
414
+ if archive_sha256:
415
+ marker["archiveSha256"] = archive_sha256
416
+ atomic_json(destination / MANAGED_SKILL_MARKER, marker)
417
+ legacy = destination / LEGACY_SKILL_MARKER
418
+ if legacy.is_file():
419
+ legacy.unlink()
420
+ return marker
421
+
422
+
423
+ def backup_skill(destination: Path, project_root: Path) -> Path:
424
+ backup_root = project_root / ".foggy" / "onboarding-backups"
425
+ backup_root.mkdir(parents=True, exist_ok=True)
426
+ backup = backup_root / f"{destination.name}-{dt.datetime.now().strftime('%Y%m%d-%H%M%S-%f')}"
427
+ shutil.move(str(destination), str(backup))
428
+ return backup
429
+
430
+
431
+ def project_context_path(project_root: Path) -> Path:
432
+ return project_root / ".foggy" / "deepseek-harness" / "context.json"
433
+
434
+
435
+ def project_relative(path: Path, project_root: Path) -> str:
436
+ try:
437
+ return path.resolve(strict=False).relative_to(project_root.resolve(strict=False)).as_posix()
438
+ except ValueError:
439
+ return str(path)
440
+
441
+
442
+ def write_project_context(state: dict) -> Path:
443
+ project_root = normalized(state["projectRoot"])
444
+ install_root = normalized(state["installRoot"])
445
+ data_root = normalized(state["dataRoot"])
446
+ context_path = project_context_path(project_root)
447
+ skills = {}
448
+ for kind in ("onboarding", "analysis"):
449
+ item = state["skills"][kind]
450
+ skill_path = normalized(item["path"])
451
+ skills[kind] = {
452
+ "version": item.get("version"),
453
+ "path": project_relative(skill_path, project_root),
454
+ "absolutePath": str(skill_path),
455
+ "markerPath": project_relative(skill_path / MANAGED_SKILL_MARKER, project_root),
456
+ "managed": bool(item.get("managed")),
457
+ }
458
+ payload = {
459
+ "schemaVersion": CONTEXT_SCHEMA,
460
+ "managedBy": "@foggy-projects/deepseek-harness-plugin",
461
+ "packageVersion": state["packageVersion"],
462
+ "generatedAt": now_utc(),
463
+ "projectRoot": str(project_root),
464
+ "installStatePath": str(install_root / "install-state.json"),
465
+ "runtimeStatePath": str(data_root / "runtime-state.json"),
466
+ "operationProgressPath": str(data_root / "operation-progress.json"),
467
+ "cli": state["cli"],
468
+ "launcher": state["launcher"],
469
+ "skills": skills,
470
+ "securityMode": state["securityMode"],
471
+ "productionReady": False,
472
+ }
473
+ atomic_json(context_path, payload)
474
+ return context_path
475
+
476
+
225
477
  def read_json_object(path: Path, label: str) -> dict:
226
478
  if not path.is_file():
227
479
  raise OnboardingError(f"{label} not found: {path}")
@@ -512,22 +764,16 @@ def safe_extract(zip_path: Path, destination: Path) -> None:
512
764
  archive.extractall(destination)
513
765
 
514
766
 
515
- def install_analysis_skill(zip_path: Path, project_root: Path, version: str, expected_hash: str, replace: bool) -> dict:
767
+ def install_analysis_skill(
768
+ zip_path: Path,
769
+ project_root: Path,
770
+ version: str,
771
+ expected_hash: str,
772
+ package_version: str,
773
+ replace: bool,
774
+ ) -> dict:
516
775
  skills_root = project_root / ".agents" / "skills"
517
776
  destination = skills_root / "foggy-ai-analysis"
518
- marker = destination / ".foggy-onboarding-install.json"
519
- if destination.exists():
520
- if marker.is_file():
521
- installed = json.loads(marker.read_text(encoding="utf-8"))
522
- if installed.get("archiveSha256") == expected_hash:
523
- return {"path": str(destination), "version": version, "action": "kept-matching"}
524
- if not replace:
525
- raise OnboardingError(f"Analysis Skill already exists and is not managed at {destination}; rerun with --replace-skill to back it up")
526
- backup_root = project_root / ".foggy" / "onboarding-backups"
527
- backup_root.mkdir(parents=True, exist_ok=True)
528
- backup = backup_root / f"foggy-ai-analysis-{dt.datetime.now().strftime('%Y%m%d-%H%M%S')}"
529
- shutil.move(str(destination), str(backup))
530
- skills_root.mkdir(parents=True, exist_ok=True)
531
777
  with tempfile.TemporaryDirectory(prefix="foggy-skill-") as temporary:
532
778
  extract_root = Path(temporary)
533
779
  safe_extract(zip_path, extract_root)
@@ -535,21 +781,81 @@ def install_analysis_skill(zip_path: Path, project_root: Path, version: str, exp
535
781
  if len(candidates) != 1:
536
782
  raise OnboardingError(f"Expected exactly one SKILL.md in analysis Skill archive, found {len(candidates)}")
537
783
  source = candidates[0].parent
784
+ source_digest = skill_tree_digest(source)
785
+ if destination.exists():
786
+ installed = read_skill_marker(destination)
787
+ actual_digest = skill_tree_digest(destination)
788
+ marker_matches = bool(
789
+ installed
790
+ and installed.get("schemaVersion") in (MANAGED_SKILL_SCHEMA, "foggy-installed-skill/v1")
791
+ and installed.get("archiveSha256") == expected_hash
792
+ and (installed.get("sourceDigest") or installed.get("installedDigest") or actual_digest) == source_digest
793
+ and actual_digest == source_digest
794
+ )
795
+ if marker_matches:
796
+ marker = write_skill_marker(
797
+ destination,
798
+ kind="analysis",
799
+ package_version=package_version,
800
+ component_version=version,
801
+ source_digest=source_digest,
802
+ archive_sha256=expected_hash,
803
+ )
804
+ return {"path": str(destination), "version": version, "digest": marker["installedDigest"], "managed": True, "action": "kept-matching"}
805
+ if not replace:
806
+ raise OnboardingError(f"Analysis Skill is missing, modified, or outdated at {destination}; use the plugin Repair action to back it up and restore it")
807
+ backup_skill(destination, project_root)
808
+ skills_root.mkdir(parents=True, exist_ok=True)
538
809
  shutil.copytree(source, destination)
539
- atomic_json(marker, {"schemaVersion": "foggy-installed-skill/v1", "version": version, "archiveSha256": expected_hash})
540
- return {"path": str(destination), "version": version, "action": "installed"}
810
+ marker = write_skill_marker(
811
+ destination,
812
+ kind="analysis",
813
+ package_version=package_version,
814
+ component_version=version,
815
+ source_digest=source_digest,
816
+ archive_sha256=expected_hash,
817
+ )
818
+ return {"path": str(destination), "version": version, "digest": marker["installedDigest"], "managed": True, "action": "installed"}
541
819
 
542
820
 
543
- def install_onboarding_skill(project_root: Path) -> dict:
821
+ def install_onboarding_skill(project_root: Path, package_version: str, replace: bool) -> dict:
544
822
  destination = project_root / ".agents" / "skills" / "foggy-deepseek-onboarding"
545
823
  source = skill_root()
824
+ source_digest = skill_tree_digest(source)
546
825
  if source.resolve() == destination.resolve(strict=False):
547
- return {"path": str(destination), "action": "already-running-from-target"}
826
+ return {"path": str(destination), "version": package_version, "digest": source_digest, "managed": True, "action": "already-running-from-target"}
548
827
  if destination.exists():
549
- return {"path": str(destination), "action": "kept-existing"}
828
+ marker = read_skill_marker(destination)
829
+ actual_digest = skill_tree_digest(destination)
830
+ if (
831
+ marker
832
+ and marker.get("schemaVersion") == MANAGED_SKILL_SCHEMA
833
+ and marker.get("kind") == "onboarding"
834
+ and actual_digest == source_digest
835
+ and marker.get("sourceDigest") in (None, source_digest)
836
+ and marker.get("installedDigest") in (None, source_digest)
837
+ ):
838
+ written = write_skill_marker(
839
+ destination,
840
+ kind="onboarding",
841
+ package_version=package_version,
842
+ component_version=package_version,
843
+ source_digest=source_digest,
844
+ )
845
+ return {"path": str(destination), "version": package_version, "digest": written["installedDigest"], "managed": True, "action": "kept-matching"}
846
+ if not replace:
847
+ raise OnboardingError(f"Onboarding Skill is missing, modified, or outdated at {destination}; use the plugin Repair action to back it up and restore it")
848
+ backup_skill(destination, project_root)
550
849
  destination.parent.mkdir(parents=True, exist_ok=True)
551
850
  shutil.copytree(source, destination)
552
- return {"path": str(destination), "action": "installed"}
851
+ marker = write_skill_marker(
852
+ destination,
853
+ kind="onboarding",
854
+ package_version=package_version,
855
+ component_version=package_version,
856
+ source_digest=source_digest,
857
+ )
858
+ return {"path": str(destination), "version": package_version, "digest": marker["installedDigest"], "managed": True, "action": "installed"}
553
859
 
554
860
 
555
861
  def read_install_state(install_root: Path, required: bool = True) -> dict | None:
@@ -574,6 +880,7 @@ def java_probe() -> dict:
574
880
 
575
881
 
576
882
  def install_command(args: argparse.Namespace) -> dict:
883
+ global ACTIVE_PROGRESS
577
884
  versions = load_versions()
578
885
  install_root = normalized(args.install_root or default_install_root())
579
886
  data_root = normalized(args.data_root or default_data_root())
@@ -595,6 +902,13 @@ def install_command(args: argparse.Namespace) -> dict:
595
902
  }
596
903
  if args.dry_run:
597
904
  return {"success": True, "dryRun": True, "plan": plan}
905
+ progress = ProgressReporter(
906
+ getattr(args, "progress_file", None),
907
+ getattr(args, "operation_id", None),
908
+ getattr(args, "operation_kind", "initialize"),
909
+ )
910
+ ACTIVE_PROGRESS = progress
911
+ progress.update("preflight", 0, "Checking prerequisites")
598
912
  if sys.version_info < (3, 11):
599
913
  raise OnboardingError(f"Python 3.11+ required, got {sys.version.split()[0]}")
600
914
  if not project_root.is_dir():
@@ -605,20 +919,37 @@ def install_command(args: argparse.Namespace) -> dict:
605
919
  verified: list[dict] = []
606
920
 
607
921
  cli_component = components["cli"]
608
- for role in ("wheel", "checksums"):
609
- asset = cli_component[role]
610
- verified.append(materialize(asset, downloads / "cli" / asset["file"], cache_dirs))
922
+ cli_assets = [cli_component[role] for role in ("wheel", "checksums")]
923
+ for index, asset in enumerate(cli_assets):
924
+ progress.update(
925
+ "cli", 1, "Downloading and verifying CLI",
926
+ fraction=index / len(cli_assets), current_file=asset["file"],
927
+ completed_files=index, total_files=len(cli_assets),
928
+ )
929
+ verified.append(materialize(
930
+ asset, downloads / "cli" / asset["file"], cache_dirs,
931
+ progress=progress, progress_phase="cli", progress_step=1,
932
+ progress_index=index, progress_total=len(cli_assets),
933
+ progress_message="Downloading and verifying CLI",
934
+ ))
935
+ progress.update(
936
+ "cli", 1, "Downloading and verifying CLI",
937
+ fraction=(index + 1) / len(cli_assets), current_file=asset["file"],
938
+ completed_files=index + 1, total_files=len(cli_assets),
939
+ )
611
940
  checksum_lines = (downloads / "cli" / cli_component["checksums"]["file"]).read_text(encoding="utf-8").splitlines()
612
941
  checksum_entries = {line.split(maxsplit=1)[1].strip(): line.split(maxsplit=1)[0].lower() for line in checksum_lines if len(line.split(maxsplit=1)) == 2}
613
942
  wheel_asset = cli_component["wheel"]
614
943
  if checksum_entries.get(wheel_asset["file"]) != wheel_asset["sha256"]:
615
944
  raise OnboardingError("Pinned CLI SHA256SUMS does not match the pinned wheel hash")
616
945
  if args.skip_cli_install:
946
+ progress.update("cli", 1, "Using existing CLI", fraction=0.65, current_file="foggy-runtime")
617
947
  cli_command = normalized(args.cli_command or shutil.which("foggy-runtime") or "")
618
948
  if not cli_command.is_file():
619
949
  raise OnboardingError("--skip-cli-install requires --cli-command or foggy-runtime on PATH")
620
950
  cli_mode = "external"
621
951
  else:
952
+ progress.update("cli", 1, "Installing CLI", fraction=0.65, current_file="Python virtual environment")
622
953
  python_path = venv_python(install_root)
623
954
  if not python_path.is_file():
624
955
  venv.EnvBuilder(with_pip=True).create(install_root / "venv")
@@ -633,27 +964,66 @@ def install_command(args: argparse.Namespace) -> dict:
633
964
  )
634
965
  cli_command = venv_cli(install_root)
635
966
  cli_mode = "managed-venv"
967
+ progress.update("cli", 1, "Verifying CLI version", fraction=0.9, current_file="foggy-runtime")
636
968
  cli_version = command_result([str(cli_command), "--version"], check=True)
637
969
  actual_cli_version = version_tuple(cli_version["stdout"])[:3]
638
970
  pinned_cli_version = version_tuple(cli_component["version"])[:3]
639
971
  version_matches = actual_cli_version >= pinned_cli_version if cli_mode == "external" else actual_cli_version == pinned_cli_version
640
972
  if not version_matches:
641
973
  raise OnboardingError(f"Unexpected CLI version: {cli_version['stdout']}")
974
+ progress.update("cli", 1, "CLI ready", fraction=1.0)
642
975
 
643
976
  launcher_dir = install_root / "launcher"
644
- for asset in components["launcher"]["assets"]:
645
- verified.append(materialize(asset, launcher_dir / asset["file"], cache_dirs))
977
+ launcher_assets = components["launcher"]["assets"]
978
+ for index, asset in enumerate(launcher_assets):
979
+ progress.update(
980
+ "launcher", 2, "Downloading and verifying Launcher",
981
+ fraction=index / len(launcher_assets), current_file=asset["file"],
982
+ completed_files=index, total_files=len(launcher_assets),
983
+ )
984
+ verified.append(materialize(
985
+ asset, launcher_dir / asset["file"], cache_dirs,
986
+ progress=progress, progress_phase="launcher", progress_step=2,
987
+ progress_index=index, progress_total=len(launcher_assets),
988
+ progress_message="Downloading and verifying Launcher",
989
+ ))
990
+ progress.update(
991
+ "launcher", 2, "Downloading and verifying Launcher",
992
+ fraction=(index + 1) / len(launcher_assets), current_file=asset["file"],
993
+ completed_files=index + 1, total_files=len(launcher_assets),
994
+ )
646
995
  if os.name != "nt":
647
996
  (launcher_dir / "start-foggy-runtime.sh").chmod(0o755)
997
+ progress.update("launcher", 2, "Launcher ready", fraction=1.0)
648
998
 
649
999
  analysis_assets = components["analysisSkill"]["assets"]
650
- for asset in analysis_assets:
651
- verified.append(materialize(asset, downloads / "skill" / asset["file"], cache_dirs))
1000
+ for index, asset in enumerate(analysis_assets):
1001
+ progress.update(
1002
+ "analysis-skill", 3, "Downloading and verifying analysis Skill",
1003
+ fraction=index / len(analysis_assets), current_file=asset["file"],
1004
+ completed_files=index, total_files=len(analysis_assets),
1005
+ )
1006
+ verified.append(materialize(
1007
+ asset, downloads / "skill" / asset["file"], cache_dirs,
1008
+ progress=progress, progress_phase="analysis-skill", progress_step=3,
1009
+ progress_index=index, progress_total=len(analysis_assets),
1010
+ progress_message="Downloading and verifying analysis Skill",
1011
+ ))
1012
+ progress.update(
1013
+ "analysis-skill", 3, "Downloading and verifying analysis Skill",
1014
+ fraction=(index + 1) / len(analysis_assets), current_file=asset["file"],
1015
+ completed_files=index + 1, total_files=len(analysis_assets),
1016
+ )
652
1017
  zip_asset = next(item for item in analysis_assets if item["role"] == "zip")
1018
+ progress.update("analysis-skill", 3, "Installing analysis Skill", fraction=0.9, current_file=zip_asset["file"])
653
1019
  analysis_skill = install_analysis_skill(
654
- downloads / "skill" / zip_asset["file"], project_root, components["analysisSkill"]["version"], zip_asset["sha256"], args.replace_skill
1020
+ downloads / "skill" / zip_asset["file"], project_root, components["analysisSkill"]["version"],
1021
+ zip_asset["sha256"], versions["packageVersion"], args.replace_skill,
655
1022
  )
656
- onboarding_skill = install_onboarding_skill(project_root)
1023
+ progress.update("analysis-skill", 3, "Analysis Skill ready", fraction=1.0)
1024
+ progress.update("workspace-skills", 4, "Installing onboarding Skill", fraction=0.1)
1025
+ onboarding_skill = install_onboarding_skill(project_root, versions["packageVersion"], args.replace_skill)
1026
+ progress.update("workspace-skills", 4, "Workspace Skills ready", fraction=1.0)
657
1027
  state = {
658
1028
  "schemaVersion": STATE_SCHEMA,
659
1029
  "installedAt": now_utc(),
@@ -661,6 +1031,7 @@ def install_command(args: argparse.Namespace) -> dict:
661
1031
  "installRoot": str(install_root),
662
1032
  "dataRoot": str(data_root),
663
1033
  "projectRoot": str(project_root),
1034
+ "contextPath": str(project_context_path(project_root)),
664
1035
  "cli": {"version": cli_component["version"], "command": str(cli_command), "mode": cli_mode},
665
1036
  "launcher": {"version": components["launcher"]["version"], "path": str(launcher_dir)},
666
1037
  "skills": {"onboarding": onboarding_skill, "analysis": analysis_skill},
@@ -668,7 +1039,12 @@ def install_command(args: argparse.Namespace) -> dict:
668
1039
  "securityMode": versions["defaults"]["securityMode"],
669
1040
  "productionReady": False,
670
1041
  }
1042
+ progress.update("state", 5, "Writing install state", fraction=0.2, current_file="install-state.json")
671
1043
  atomic_json(install_root / "install-state.json", state)
1044
+ progress.update("state", 5, "Writing project context", fraction=0.7, current_file=".foggy/deepseek-harness/context.json")
1045
+ context_path = write_project_context(state)
1046
+ progress.finish()
1047
+ ACTIVE_PROGRESS = None
672
1048
  return {
673
1049
  "success": True,
674
1050
  "schemaVersion": "foggy-deepseek-onboarding-install-result/v1",
@@ -676,6 +1052,7 @@ def install_command(args: argparse.Namespace) -> dict:
676
1052
  "installRoot": str(install_root),
677
1053
  "dataRoot": str(data_root),
678
1054
  "projectRoot": str(project_root),
1055
+ "contextPath": str(context_path),
679
1056
  "cliVersion": cli_component["version"],
680
1057
  "launcherVersion": components["launcher"]["version"],
681
1058
  "analysisSkill": analysis_skill,
@@ -1857,8 +2234,24 @@ def doctor_command(args: argparse.Namespace) -> dict:
1857
2234
  path = launcher_dir / asset["file"]
1858
2235
  launcher_checks.append({"file": asset["file"], "present": path.is_file(), "sha256Valid": path.is_file() and sha256(path) == asset["sha256"]})
1859
2236
  launcher_ok = bool(launcher_checks) and all(item["present"] and item["sha256Valid"] for item in launcher_checks)
1860
- analysis_skill = project_root / ".agents" / "skills" / "foggy-ai-analysis" / "SKILL.md"
1861
- onboarding_skill = project_root / ".agents" / "skills" / "foggy-deepseek-onboarding" / "SKILL.md"
2237
+ analysis_skill_root = project_root / ".agents" / "skills" / "foggy-ai-analysis"
2238
+ onboarding_skill_root = project_root / ".agents" / "skills" / "foggy-deepseek-onboarding"
2239
+ analysis_skill = managed_skill_status(analysis_skill_root, "analysis", versions["components"]["analysisSkill"]["version"])
2240
+ onboarding_skill = managed_skill_status(onboarding_skill_root, "onboarding", versions["packageVersion"])
2241
+ context_path = project_context_path(project_root)
2242
+ context = None
2243
+ if context_path.is_file():
2244
+ try:
2245
+ context = json.loads(context_path.read_text(encoding="utf-8"))
2246
+ except (OSError, json.JSONDecodeError):
2247
+ context = None
2248
+ context_ok = bool(
2249
+ context
2250
+ and context.get("schemaVersion") == CONTEXT_SCHEMA
2251
+ and context.get("packageVersion") == versions["packageVersion"]
2252
+ and normalized(context.get("projectRoot", "")) == project_root
2253
+ and normalized(context.get("installStatePath", "")) == install_root / "install-state.json"
2254
+ )
1862
2255
  runtime = {"status": "stopped"}
1863
2256
  if state:
1864
2257
  data_root = normalized(state["dataRoot"])
@@ -1872,7 +2265,9 @@ def doctor_command(args: argparse.Namespace) -> dict:
1872
2265
  "java": java_ok,
1873
2266
  "cli": cli_ok,
1874
2267
  "launcher": launcher_ok,
1875
- "analysisSkill": analysis_skill.is_file(),
2268
+ "analysisSkill": analysis_skill["valid"],
2269
+ "onboardingSkill": onboarding_skill["valid"],
2270
+ "projectContext": context_ok,
1876
2271
  }
1877
2272
  if args.strict_runtime:
1878
2273
  required["runtime"] = runtime["status"] == "running"
@@ -1886,7 +2281,8 @@ def doctor_command(args: argparse.Namespace) -> dict:
1886
2281
  "java": java,
1887
2282
  "cli": cli,
1888
2283
  "launcherAssets": launcher_checks,
1889
- "skills": {"analysis": str(analysis_skill), "onboarding": str(onboarding_skill), "onboardingPresent": onboarding_skill.is_file()},
2284
+ "skills": {"analysis": analysis_skill, "onboarding": onboarding_skill},
2285
+ "projectContext": {"path": str(context_path), "valid": context_ok},
1890
2286
  "runtime": runtime,
1891
2287
  "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")},
1892
2288
  "productionReady": False,
@@ -1945,6 +2341,9 @@ def build_parser() -> argparse.ArgumentParser:
1945
2341
  install.add_argument("--replace-skill", action="store_true")
1946
2342
  install.add_argument("--skip-cli-install", action="store_true")
1947
2343
  install.add_argument("--cli-command")
2344
+ install.add_argument("--progress-file")
2345
+ install.add_argument("--operation-id")
2346
+ install.add_argument("--operation-kind", default="initialize")
1948
2347
  install.add_argument("--dry-run", action="store_true")
1949
2348
  install.set_defaults(handler=install_command)
1950
2349
 
@@ -2104,8 +2503,12 @@ def main() -> None:
2104
2503
  exit_code = 1
2105
2504
  emit(result, exit_code)
2106
2505
  except OnboardingError as exc:
2506
+ if ACTIVE_PROGRESS is not None:
2507
+ ACTIVE_PROGRESS.fail(exc)
2107
2508
  emit({"success": False, "error": {"code": "ONBOARDING_ERROR", "message": str(exc)}, "productionReady": False}, 1)
2108
2509
  except Exception as exc:
2510
+ if ACTIVE_PROGRESS is not None:
2511
+ ACTIVE_PROGRESS.fail(exc)
2109
2512
  emit({"success": False, "error": {"code": "UNEXPECTED_ERROR", "message": str(exc)}, "productionReady": False}, 1)
2110
2513
 
2111
2514