@foggy-projects/deepseek-harness-plugin 0.4.0-beta.3 → 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.3.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.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
+ projectContext: '项目安装上下文',
44
46
  python: 'Python',
45
47
  java: 'Java',
46
48
  installed: '已安装',
@@ -68,8 +70,9 @@ window.__ModuleLoader__.load({
68
70
  dataRoot: '数据目录',
69
71
  runtimeUrl: 'Runtime 地址',
70
72
  projectRoot: 'Skill 目标工作区',
73
+ contextPath: '对话发现文件',
71
74
  nextTitle: '后续配置',
72
- nextCopy: 'Runtime 启动成功后,将继续进入数据库连接与语义层向导。数据库密码不会写入 DSH 普通设置。',
75
+ nextCopy: '对话会从项目内固定的 context.json 读取 CLI、Runtime Skill 安装状态,不再依赖 PATH 猜测。Runtime 启动成功后,将继续进入数据库连接与语义层向导。',
73
76
  beta: 'Beta',
74
77
  }
75
78
 
@@ -88,6 +91,8 @@ window.__ModuleLoader__.load({
88
91
  cli: 'CLI',
89
92
  launcher: 'Launcher',
90
93
  analysisSkill: 'Analysis Skill',
94
+ onboardingSkill: 'Onboarding Skill',
95
+ projectContext: 'Project install context',
91
96
  python: 'Python',
92
97
  java: 'Java',
93
98
  installed: 'Installed',
@@ -115,8 +120,9 @@ window.__ModuleLoader__.load({
115
120
  dataRoot: 'Data',
116
121
  runtimeUrl: 'Runtime URL',
117
122
  projectRoot: 'Skill target workspace',
123
+ contextPath: 'Conversation discovery file',
118
124
  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.',
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.',
120
126
  beta: 'Beta',
121
127
  }
122
128
 
@@ -264,8 +270,8 @@ window.__ModuleLoader__.load({
264
270
  jsxs('div', { className: 'foggy-state', children: [jsx('span', { className: 'foggy-dot', 'data-state': status.state }), jsx('span', { children: t(stateLabels[status.state] || 'notInstalled') })] }),
265
271
  jsxs('div', { className: 'foggy-actions', children: [
266
272
  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,
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,
269
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,
270
276
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
271
277
  ] }),
@@ -278,6 +284,8 @@ window.__ModuleLoader__.load({
278
284
  componentCard(t('cli'), status.components.cli, 'runtime', t),
279
285
  componentCard(t('launcher'), status.components.launcher, 'runtime', t),
280
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),
281
289
  ] }),
282
290
  jsxs('div', { className: 'foggy-paths', children: [
283
291
  jsx('h4', { children: t('roots') }),
@@ -285,6 +293,7 @@ window.__ModuleLoader__.load({
285
293
  jsx('dt', { children: t('installRoot') }), jsx('dd', { children: status.roots.installRoot }),
286
294
  jsx('dt', { children: t('dataRoot') }), jsx('dd', { children: status.roots.dataRoot }),
287
295
  jsx('dt', { children: t('projectRoot') }), jsx('dd', { children: status.projectRoot }),
296
+ jsx('dt', { children: t('contextPath') }), jsx('dd', { children: status.contextPath }),
288
297
  status.runtimeUrl ? jsx('dt', { children: t('runtimeUrl') }) : null,
289
298
  status.runtimeUrl ? jsx('dd', { children: status.runtimeUrl }) : null,
290
299
  ] }),
package/lib/index.js CHANGED
@@ -129,9 +129,11 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
129
129
 
130
130
  async status() {
131
131
  const roots = defaultRoots()
132
+ const projectRoot = process.env.FOGGY_PROJECT_ROOT || process.cwd()
132
133
  const statePath = join(roots.installRoot, 'install-state.json')
133
134
  const runtimeStatePath = join(roots.dataRoot, 'runtime-state.json')
134
135
  const progressPath = join(roots.dataRoot, 'operation-progress.json')
136
+ const contextPath = join(projectRoot, '.foggy', 'deepseek-harness', 'context.json')
135
137
  const manifest = await readJson(versionsFile)
136
138
  let pythonProbe
137
139
  try {
@@ -143,10 +145,16 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
143
145
  const java = compatible(await commandVersion(process.env.JAVA_EXE || 'java', ['-version']), '17.0')
144
146
  let state = null
145
147
  let runtime = null
148
+ let context = null
146
149
  try { state = await readJson(statePath) } catch {}
147
150
  try { runtime = await readJson(runtimeStatePath) } catch {}
151
+ try { context = await readJson(contextPath) } catch {}
148
152
  const cliPath = state?.cli?.command
149
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'))
150
158
  const components = {
151
159
  python,
152
160
  java,
@@ -159,11 +167,36 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
159
167
  version: state?.launcher?.version ?? manifest.components.launcher.version,
160
168
  },
161
169
  analysisSkill: {
162
- 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
+ ),
163
175
  version: state?.skills?.analysis?.version ?? manifest.components.analysisSkill.version,
164
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
+ },
165
194
  }
166
- 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
167
200
  const running = Boolean(runtime && processRunning(Number(runtime.pid)))
168
201
  const progress = await readOptionalJson(progressPath)
169
202
  let operation = operationView(this.operation)
@@ -189,7 +222,8 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
189
222
  running,
190
223
  runtimeUrl: running ? runtime.runtimeUrl ?? null : null,
191
224
  roots,
192
- projectRoot: process.env.FOGGY_PROJECT_ROOT || process.cwd(),
225
+ projectRoot,
226
+ contextPath,
193
227
  components,
194
228
  operation,
195
229
  next: installed ? (running ? 'configure-database' : 'start-runtime') : 'initialize',
@@ -208,7 +242,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
208
242
  'create isolated Python environment',
209
243
  'download and verify pinned CLI and Launcher assets',
210
244
  'install Foggy analysis and onboarding Skills into the workspace',
211
- 'write local install state',
245
+ 'write global install state and project-relative discovery context',
212
246
  ],
213
247
  secretsInDshSettings: false,
214
248
  }
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.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.3",
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}$")
@@ -331,6 +335,145 @@ def atomic_json(path: Path, payload: dict) -> None:
331
335
  os.replace(temporary, path)
332
336
 
333
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
+
334
477
  def read_json_object(path: Path, label: str) -> dict:
335
478
  if not path.is_file():
336
479
  raise OnboardingError(f"{label} not found: {path}")
@@ -621,22 +764,16 @@ def safe_extract(zip_path: Path, destination: Path) -> None:
621
764
  archive.extractall(destination)
622
765
 
623
766
 
624
- 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:
625
775
  skills_root = project_root / ".agents" / "skills"
626
776
  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
777
  with tempfile.TemporaryDirectory(prefix="foggy-skill-") as temporary:
641
778
  extract_root = Path(temporary)
642
779
  safe_extract(zip_path, extract_root)
@@ -644,21 +781,81 @@ def install_analysis_skill(zip_path: Path, project_root: Path, version: str, exp
644
781
  if len(candidates) != 1:
645
782
  raise OnboardingError(f"Expected exactly one SKILL.md in analysis Skill archive, found {len(candidates)}")
646
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)
647
809
  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"}
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"}
650
819
 
651
820
 
652
- def install_onboarding_skill(project_root: Path) -> dict:
821
+ def install_onboarding_skill(project_root: Path, package_version: str, replace: bool) -> dict:
653
822
  destination = project_root / ".agents" / "skills" / "foggy-deepseek-onboarding"
654
823
  source = skill_root()
824
+ source_digest = skill_tree_digest(source)
655
825
  if source.resolve() == destination.resolve(strict=False):
656
- 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"}
657
827
  if destination.exists():
658
- 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)
659
849
  destination.parent.mkdir(parents=True, exist_ok=True)
660
850
  shutil.copytree(source, destination)
661
- 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"}
662
859
 
663
860
 
664
861
  def read_install_state(install_root: Path, required: bool = True) -> dict | None:
@@ -820,11 +1017,12 @@ def install_command(args: argparse.Namespace) -> dict:
820
1017
  zip_asset = next(item for item in analysis_assets if item["role"] == "zip")
821
1018
  progress.update("analysis-skill", 3, "Installing analysis Skill", fraction=0.9, current_file=zip_asset["file"])
822
1019
  analysis_skill = install_analysis_skill(
823
- 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,
824
1022
  )
825
1023
  progress.update("analysis-skill", 3, "Analysis Skill ready", fraction=1.0)
826
1024
  progress.update("workspace-skills", 4, "Installing onboarding Skill", fraction=0.1)
827
- onboarding_skill = install_onboarding_skill(project_root)
1025
+ onboarding_skill = install_onboarding_skill(project_root, versions["packageVersion"], args.replace_skill)
828
1026
  progress.update("workspace-skills", 4, "Workspace Skills ready", fraction=1.0)
829
1027
  state = {
830
1028
  "schemaVersion": STATE_SCHEMA,
@@ -833,6 +1031,7 @@ def install_command(args: argparse.Namespace) -> dict:
833
1031
  "installRoot": str(install_root),
834
1032
  "dataRoot": str(data_root),
835
1033
  "projectRoot": str(project_root),
1034
+ "contextPath": str(project_context_path(project_root)),
836
1035
  "cli": {"version": cli_component["version"], "command": str(cli_command), "mode": cli_mode},
837
1036
  "launcher": {"version": components["launcher"]["version"], "path": str(launcher_dir)},
838
1037
  "skills": {"onboarding": onboarding_skill, "analysis": analysis_skill},
@@ -842,6 +1041,8 @@ def install_command(args: argparse.Namespace) -> dict:
842
1041
  }
843
1042
  progress.update("state", 5, "Writing install state", fraction=0.2, current_file="install-state.json")
844
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)
845
1046
  progress.finish()
846
1047
  ACTIVE_PROGRESS = None
847
1048
  return {
@@ -851,6 +1052,7 @@ def install_command(args: argparse.Namespace) -> dict:
851
1052
  "installRoot": str(install_root),
852
1053
  "dataRoot": str(data_root),
853
1054
  "projectRoot": str(project_root),
1055
+ "contextPath": str(context_path),
854
1056
  "cliVersion": cli_component["version"],
855
1057
  "launcherVersion": components["launcher"]["version"],
856
1058
  "analysisSkill": analysis_skill,
@@ -2032,8 +2234,24 @@ def doctor_command(args: argparse.Namespace) -> dict:
2032
2234
  path = launcher_dir / asset["file"]
2033
2235
  launcher_checks.append({"file": asset["file"], "present": path.is_file(), "sha256Valid": path.is_file() and sha256(path) == asset["sha256"]})
2034
2236
  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"
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
+ )
2037
2255
  runtime = {"status": "stopped"}
2038
2256
  if state:
2039
2257
  data_root = normalized(state["dataRoot"])
@@ -2047,7 +2265,9 @@ def doctor_command(args: argparse.Namespace) -> dict:
2047
2265
  "java": java_ok,
2048
2266
  "cli": cli_ok,
2049
2267
  "launcher": launcher_ok,
2050
- "analysisSkill": analysis_skill.is_file(),
2268
+ "analysisSkill": analysis_skill["valid"],
2269
+ "onboardingSkill": onboarding_skill["valid"],
2270
+ "projectContext": context_ok,
2051
2271
  }
2052
2272
  if args.strict_runtime:
2053
2273
  required["runtime"] = runtime["status"] == "running"
@@ -2061,7 +2281,8 @@ def doctor_command(args: argparse.Namespace) -> dict:
2061
2281
  "java": java,
2062
2282
  "cli": cli,
2063
2283
  "launcherAssets": launcher_checks,
2064
- "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},
2065
2286
  "runtime": runtime,
2066
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")},
2067
2288
  "productionReady": False,