@foggy-projects/deepseek-harness-plugin 0.4.0-beta.10 → 0.4.0-beta.12

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
@@ -14,7 +14,7 @@ modify `PATH`, or register Python globally. Advanced users may explicitly set
14
14
  ## Local beta installation
15
15
 
16
16
  ```powershell
17
- dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.10.tgz
17
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.12.tgz
18
18
  ```
19
19
 
20
20
  Restart `dsh web`, use the browser it opens (or the complete printed URL,
@@ -1,10 +1,10 @@
1
- # Public beta readiness: 0.4.0-beta.10
1
+ # Public beta readiness: 0.4.0-beta.12
2
2
 
3
3
  Assessment date: 2026-09-04
4
4
 
5
5
  ## Verdict
6
6
 
7
- `0.4.0-beta.10` is suitable for a scoped public beta on Windows 10/11 x64 and
7
+ `0.4.0-beta.12` is suitable for a scoped public beta on Windows 10/11 x64 and
8
8
  Linux/WSL2 x64. It is not a general-availability release.
9
9
 
10
10
  The plugin package stays small and downloads large components only during
@@ -21,13 +21,18 @@ system PATH, registry, or a user's existing Python installation.
21
21
  - The component installer code is unchanged from beta.9, whose Windows x64 run
22
22
  completed a cold Python download, checksum verification, extraction,
23
23
  initialization, forced Python repair, and doctor run.
24
- - DeepSeek Harness `0.1.2-rc.1` loaded the beta.10 candidate Bundle, web client,
24
+ - DeepSeek Harness `0.1.2-rc.1` loaded the beta.12 candidate Bundle, web client,
25
25
  remote status service, and native Skill provider on both Windows and WSL2.
26
26
  - WSL2 reported the existing managed Python, CLI, Launcher, and analysis Skill
27
27
  as ready; an isolated Windows profile correctly reported its fresh component
28
28
  state and rejected the host's Java 12 as below the Java 17 prerequisite.
29
29
  - The UI reported Python 3.12.13 as `Foggy private`, and CLI 0.1.23, Launcher
30
- 0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.10 as installed.
30
+ 0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.12 as installed.
31
+ - Runtime startup now reports the current phase, elapsed/timeout seconds, and a
32
+ bounded progress value. A real WSL2 launch reached readiness in 13.4 seconds;
33
+ a forced timeout recorded failure evidence and cleaned up the Java process.
34
+ - Diagnostic export success remains visible alongside a prior Runtime startup
35
+ error, including the generated local JSON path.
31
36
  - Interrupted downloads resume with HTTP Range when the server supports it;
32
37
  downloaded artifacts are not trusted until the complete SHA256 matches.
33
38
  - Windows progress writes tolerate transient sharing violations without losing
@@ -1,7 +1,7 @@
1
1
  # Native Windows Beta acceptance
2
2
 
3
3
  This runbook validates DeepSeek Harness `0.1.2-rc.1` with Foggy plugin
4
- `0.4.0-beta.10` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
4
+ `0.4.0-beta.12` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
5
5
  directory that is not synchronized by OneDrive.
6
6
 
7
7
  ## Prerequisites
@@ -49,10 +49,16 @@ Expected component state:
49
49
  - CLI 0.1.23;
50
50
  - Launcher 0.1.18;
51
51
  - analysis Skill 0.1.17;
52
- - onboarding Skill 0.4.0-beta.10;
52
+ - onboarding Skill 0.4.0-beta.12;
53
53
  - Java 17+ available;
54
54
  - native DSH Skill registration available.
55
55
 
56
+ Runtime startup shows its current phase, elapsed time, and the 180-second
57
+ readiness deadline. Typical startup is 15–30 seconds on the validated WSL2 host
58
+ and may take up to 60 seconds on Windows with cold JVM or antivirus scanning.
59
+ On timeout, export diagnostics before retrying; the report includes the startup
60
+ phase, PID state, evidence directory, and Runtime log locations.
61
+
56
62
  ## Database and semantic-layer acceptance
57
63
 
58
64
  Keep database credentials out of this public repository. Create a private CLI
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  DSH_VERSION="0.1.2-rc.1"
5
5
  PNPM_VERSION="11.7.0"
6
- PLUGIN_VERSION="0.4.0-beta.10"
6
+ PLUGIN_VERSION="0.4.0-beta.12"
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
@@ -73,7 +73,16 @@ window.__ModuleLoader__.load({
73
73
  progressSkills: '注册 DSH Skills',
74
74
  progressState: '写入安装状态',
75
75
  progressComplete: '初始化完成',
76
+ progressRuntimePreflight: '检查 Runtime 状态',
77
+ progressRuntimeLauncher: '启动 Java Launcher',
78
+ progressRuntimeReadiness: '等待 Runtime 就绪',
79
+ progressRuntimeCapabilities: '验证 Runtime 能力',
80
+ progressRuntimeState: '保存 Runtime 状态',
81
+ progressRuntimeComplete: 'Runtime 已就绪',
76
82
  progressFiles: '文件',
83
+ progressElapsed: '已等待',
84
+ progressTimeout: '最长',
85
+ progressSeconds: '秒',
77
86
  managedPrivate: 'Foggy 私有',
78
87
  configuredOverride: '外部指定',
79
88
  accepted: '操作已开始;页面会自动刷新状态。',
@@ -148,7 +157,16 @@ window.__ModuleLoader__.load({
148
157
  progressSkills: 'Registering DSH Skills',
149
158
  progressState: 'Writing install state',
150
159
  progressComplete: 'Initialization complete',
160
+ progressRuntimePreflight: 'Checking Runtime state',
161
+ progressRuntimeLauncher: 'Starting Java Launcher',
162
+ progressRuntimeReadiness: 'Waiting for Runtime readiness',
163
+ progressRuntimeCapabilities: 'Verifying Runtime capabilities',
164
+ progressRuntimeState: 'Saving Runtime state',
165
+ progressRuntimeComplete: 'Runtime ready',
151
166
  progressFiles: 'files',
167
+ progressElapsed: 'elapsed',
168
+ progressTimeout: 'timeout',
169
+ progressSeconds: 's',
152
170
  managedPrivate: 'Foggy private',
153
171
  configuredOverride: 'Configured override',
154
172
  accepted: 'The operation started; this page will refresh automatically.',
@@ -247,21 +265,33 @@ window.__ModuleLoader__.load({
247
265
  'workspace-skills': 'progressSkills',
248
266
  state: 'progressState',
249
267
  complete: 'progressComplete',
268
+ 'runtime-preflight': 'progressRuntimePreflight',
269
+ 'runtime-launcher': 'progressRuntimeLauncher',
270
+ 'runtime-readiness': 'progressRuntimeReadiness',
271
+ 'runtime-capabilities': 'progressRuntimeCapabilities',
272
+ 'runtime-state': 'progressRuntimeState',
273
+ 'runtime-complete': 'progressRuntimeComplete',
250
274
  }
251
275
  const phase = t(phaseKeys[progress.phase] || 'progressWorking')
276
+ const headline = String(progress.phase || '').startsWith('runtime-') ? phase : progress.message || phase
252
277
  const step = progress.step?.index && progress.step?.total
253
278
  ? `${progress.step.index}/${progress.step.total}`
254
279
  : ''
255
280
  const files = progress.files?.total
256
281
  ? `${progress.files.completed || 0}/${progress.files.total} ${t('progressFiles')}`
257
282
  : ''
283
+ const elapsed = Number(progress.timing?.elapsedSeconds)
284
+ const timeout = Number(progress.timing?.timeoutSeconds)
285
+ const timing = Number.isFinite(elapsed)
286
+ ? `${t('progressElapsed')} ${elapsed}${t('progressSeconds')}${Number.isFinite(timeout) ? ` / ${t('progressTimeout')} ${timeout}${t('progressSeconds')}` : ''}`
287
+ : ''
258
288
  return jsxs('div', {
259
289
  className: 'foggy-progress',
260
290
  role: 'status',
261
291
  'aria-live': 'polite',
262
292
  children: [
263
293
  jsxs('div', { className: 'foggy-progress-head', children: [
264
- jsx('strong', { children: progress.message || phase }),
294
+ jsx('strong', { children: headline }),
265
295
  jsx('span', { className: 'foggy-progress-percent', children: `${percent}%` }),
266
296
  ] }),
267
297
  jsx('div', {
@@ -275,7 +305,7 @@ window.__ModuleLoader__.load({
275
305
  }),
276
306
  jsxs('div', { className: 'foggy-progress-meta', children: [
277
307
  jsx('span', { children: phase }),
278
- jsx('span', { children: [step, files].filter(Boolean).join(' · ') }),
308
+ jsx('span', { children: [step, files, timing].filter(Boolean).join(' · ') }),
279
309
  ] }),
280
310
  progress.currentFile ? jsx('div', { className: 'foggy-progress-meta foggy-progress-file', title: progress.currentFile, children: progress.currentFile }) : null,
281
311
  ],
@@ -383,7 +413,9 @@ window.__ModuleLoader__.load({
383
413
  ] }),
384
414
  ] }),
385
415
  progress,
386
- busy || operationError || view.message ? jsx('p', { className: 'foggy-message', 'data-error': operationError || view.error ? 'true' : undefined, children: busy ? t('working') : operationError || view.message }) : null,
416
+ busy ? jsx('p', { className: 'foggy-message', children: t('working') }) : null,
417
+ operationError ? jsx('p', { className: 'foggy-message', 'data-error': 'true', children: operationError }) : null,
418
+ !busy && view.message ? jsx('p', { className: 'foggy-message', 'data-error': view.error ? 'true' : undefined, children: view.message }) : null,
387
419
  jsxs('div', { className: 'foggy-grid', children: [
388
420
  componentCard(t('python'), status.components.python, 'tool', t),
389
421
  componentCard(t('java'), status.components.java, 'tool', t),
package/lib/index.js CHANGED
@@ -131,6 +131,8 @@ async function assertSystemPrerequisites(kind) {
131
131
 
132
132
  function createPythonProgressReporter(operation, path) {
133
133
  let pending = Promise.resolve()
134
+ const runtimeStart = operation.kind === 'runtime-start'
135
+ const totalSteps = runtimeStart ? 6 : 7
134
136
  return {
135
137
  update(detail) {
136
138
  const fraction = Math.max(0, Math.min(1, Number(detail.fraction) || 0))
@@ -139,11 +141,11 @@ function createPythonProgressReporter(operation, path) {
139
141
  operationId: operation.id,
140
142
  kind: operation.kind,
141
143
  state: 'running',
142
- phase: 'python',
143
- message: detail.message || 'Preparing managed Python',
144
+ phase: runtimeStart ? 'runtime-preflight' : 'python',
145
+ message: runtimeStart ? 'Preparing the Foggy Runtime controller' : detail.message || 'Preparing managed Python',
144
146
  currentFile: detail.currentFile || null,
145
- percent: Math.round(((1 + fraction) / 7) * 100),
146
- step: { index: 2, total: 7 },
147
+ percent: runtimeStart ? Math.round(fraction * 3) : Math.round(((1 + fraction) / totalSteps) * 100),
148
+ step: { index: runtimeStart ? 1 : 2, total: totalSteps },
147
149
  startedAt: operation.startedAt,
148
150
  updatedAt: new Date().toISOString(),
149
151
  }
@@ -348,6 +350,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
348
350
  components: status.components,
349
351
  onboarding: status.onboarding,
350
352
  profileMigration: status.profileMigration,
353
+ lastRuntimeStartFailure: await readOptionalJson(join(roots.dataRoot, 'last-runtime-start-failure.json')),
351
354
  doctor,
352
355
  }
353
356
  const directory = join(roots.dataRoot, 'diagnostics')
@@ -358,11 +361,17 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
358
361
  }
359
362
 
360
363
  async runtimeStart() {
361
- return this.startOperation('runtime-start', false, ['runtime-start'])
364
+ const roots = defaultRoots()
365
+ return this.startOperation('runtime-start', false, [
366
+ 'runtime-start', '--install-root', roots.installRoot, '--data-root', roots.dataRoot,
367
+ ])
362
368
  }
363
369
 
364
370
  async runtimeStop() {
365
- return this.startOperation('runtime-stop', false, ['runtime-stop'])
371
+ const roots = defaultRoots()
372
+ return this.startOperation('runtime-stop', false, [
373
+ 'runtime-stop', '--install-root', roots.installRoot, '--data-root', roots.dataRoot,
374
+ ])
366
375
  }
367
376
 
368
377
  startOperation(kind, replaceSkill, explicitArgs) {
@@ -371,7 +380,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
371
380
  }
372
381
  const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
373
382
  const args = explicitArgs ?? ['install']
374
- const reportsProgress = args[0] === 'install'
383
+ const reportsProgress = args[0] === 'install' || args[0] === 'runtime-start'
375
384
  if (reportsProgress) {
376
385
  const roots = defaultRoots()
377
386
  args.push('--progress-file', join(roots.dataRoot, 'operation-progress.json'), '--operation-id', id, '--operation-kind', kind)
@@ -380,6 +389,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
380
389
  for (const cacheDir of (process.env.FOGGY_ASSET_CACHE_DIRS || '').split(delimiter).filter(Boolean)) {
381
390
  args.push('--asset-cache-dir', cacheDir)
382
391
  }
392
+ const runtimeStart = kind === 'runtime-start'
383
393
  const operation = {
384
394
  id,
385
395
  kind,
@@ -394,10 +404,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
394
404
  kind,
395
405
  state: 'running',
396
406
  phase: 'preflight',
397
- message: 'Starting Foggy initialization',
407
+ message: runtimeStart ? 'Starting Foggy Runtime' : 'Starting Foggy initialization',
398
408
  currentFile: null,
399
409
  percent: 0,
400
- step: { index: 1, total: 7 },
410
+ step: { index: 1, total: runtimeStart ? 6 : 7 },
401
411
  } : null,
402
412
  promise: null,
403
413
  }
@@ -422,8 +432,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
422
432
  operation.progress = {
423
433
  ...operation.progress,
424
434
  state: operation.state,
425
- phase: operation.state === 'succeeded' ? 'complete' : operation.progress.phase,
426
- message: operation.state === 'succeeded' ? 'Foggy initialization completed' : 'Foggy initialization failed',
435
+ phase: operation.state === 'succeeded' ? (runtimeStart ? 'runtime-complete' : 'complete') : operation.progress.phase,
436
+ message: operation.state === 'succeeded'
437
+ ? (runtimeStart ? 'Foggy Runtime is ready' : 'Foggy initialization completed')
438
+ : (runtimeStart ? 'Foggy Runtime startup failed' : 'Foggy initialization failed'),
427
439
  percent: operation.state === 'succeeded' ? 100 : operation.progress.percent,
428
440
  }
429
441
  }
@@ -437,7 +449,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
437
449
  operation.progress = {
438
450
  ...operation.progress,
439
451
  state: 'failed',
440
- message: 'Foggy initialization failed',
452
+ message: runtimeStart ? 'Foggy Runtime startup failed' : 'Foggy initialization failed',
441
453
  }
442
454
  }
443
455
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foggy-projects/deepseek-harness-plugin",
3
- "version": "0.4.0-beta.10",
3
+ "version": "0.4.0-beta.12",
4
4
  "description": "Foggy Java data analysis engine integration for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "foggy-deepseek-onboarding-versions/v1",
3
- "packageVersion": "0.4.0-beta.10",
3
+ "packageVersion": "0.4.0-beta.12",
4
4
  "validatedAt": "2026-09-04",
5
5
  "components": {
6
6
  "deepseekHarness": {
@@ -48,20 +48,35 @@ ACTIVE_PROGRESS: "ProgressReporter | None" = None
48
48
 
49
49
 
50
50
  class ProgressReporter:
51
- total_steps = 7
52
-
53
- def __init__(self, path: str | None, operation_id: str | None, kind: str) -> None:
51
+ def __init__(
52
+ self,
53
+ path: str | None,
54
+ operation_id: str | None,
55
+ kind: str,
56
+ *,
57
+ total_steps: int = 7,
58
+ completion_phase: str = "complete",
59
+ completion_message: str = "Foggy initialization completed",
60
+ failure_message: str = "Foggy initialization failed",
61
+ ) -> None:
54
62
  self.path = Path(path).expanduser() if path else None
55
63
  self.operation_id = operation_id
56
64
  self.kind = kind
65
+ self.total_steps = total_steps
66
+ self.completion_phase = completion_phase
67
+ self.completion_message = completion_message
68
+ self.failure_message = failure_message
57
69
  self.started_at = now_utc()
58
70
  self.phase = "preflight"
59
71
  self.step_index = 0
60
72
  self.fraction = 0.0
73
+ self.percent_override: int | None = None
61
74
  self.message = ""
62
75
  self.current_file: str | None = None
63
76
  self.completed_files: int | None = None
64
77
  self.total_files: int | None = None
78
+ self.elapsed_seconds: int | None = None
79
+ self.timeout_seconds: int | None = None
65
80
 
66
81
  @property
67
82
  def enabled(self) -> bool:
@@ -72,7 +87,11 @@ class ProgressReporter:
72
87
  return
73
88
  bounded_step = max(0, min(self.step_index, self.total_steps))
74
89
  bounded_fraction = max(0.0, min(float(self.fraction), 1.0))
75
- percent = 100 if state == "succeeded" else round(((bounded_step + bounded_fraction) / self.total_steps) * 100)
90
+ percent = 100 if state == "succeeded" else (
91
+ max(0, min(self.percent_override, 99))
92
+ if self.percent_override is not None
93
+ else round(((bounded_step + bounded_fraction) / self.total_steps) * 100)
94
+ )
76
95
  payload = {
77
96
  "schemaVersion": PROGRESS_SCHEMA,
78
97
  "operationId": self.operation_id,
@@ -88,6 +107,10 @@ class ProgressReporter:
88
107
  }
89
108
  if self.completed_files is not None and self.total_files is not None:
90
109
  payload["files"] = {"completed": self.completed_files, "total": self.total_files}
110
+ if self.elapsed_seconds is not None:
111
+ payload["timing"] = {"elapsedSeconds": self.elapsed_seconds}
112
+ if self.timeout_seconds is not None:
113
+ payload["timing"]["timeoutSeconds"] = self.timeout_seconds
91
114
  if finished_at:
92
115
  payload["finishedAt"] = finished_at
93
116
  if error:
@@ -105,28 +128,37 @@ class ProgressReporter:
105
128
  current_file: str | None = None,
106
129
  completed_files: int | None = None,
107
130
  total_files: int | None = None,
131
+ percent: int | None = None,
132
+ elapsed_seconds: int | None = None,
133
+ timeout_seconds: int | None = None,
108
134
  ) -> None:
109
135
  self.phase = phase
110
136
  self.step_index = step_index
111
137
  self.fraction = fraction
138
+ self.percent_override = percent
112
139
  self.message = message
113
140
  self.current_file = current_file
114
141
  self.completed_files = completed_files
115
142
  self.total_files = total_files
143
+ self.elapsed_seconds = elapsed_seconds
144
+ self.timeout_seconds = timeout_seconds
116
145
  self._write("running")
117
146
 
118
147
  def finish(self) -> None:
119
- self.phase = "complete"
148
+ self.phase = self.completion_phase
120
149
  self.step_index = self.total_steps
121
150
  self.fraction = 0.0
122
- self.message = "Foggy initialization completed"
151
+ self.percent_override = 100
152
+ self.message = self.completion_message
123
153
  self.current_file = None
124
154
  self.completed_files = None
125
155
  self.total_files = None
156
+ self.elapsed_seconds = None
157
+ self.timeout_seconds = None
126
158
  self._write("succeeded", finished_at=now_utc())
127
159
 
128
160
  def fail(self, error: object) -> None:
129
- self.message = "Foggy initialization failed"
161
+ self.message = self.failure_message
130
162
  self._write("failed", finished_at=now_utc(), error=str(error))
131
163
 
132
164
 
@@ -245,6 +277,79 @@ def command_result(command: list[str], timeout: int = 30, check: bool = False) -
245
277
  return payload
246
278
 
247
279
 
280
+ def command_result_with_progress(
281
+ command: list[str],
282
+ timeout: int,
283
+ progress: ProgressReporter,
284
+ *,
285
+ phase: str,
286
+ step_index: int,
287
+ message: str,
288
+ readiness_timeout: int,
289
+ start_percent: int,
290
+ end_percent: int,
291
+ poll_interval: float = 1.0,
292
+ ) -> dict:
293
+ started = time.monotonic()
294
+ try:
295
+ process = subprocess.Popen(
296
+ command,
297
+ stdout=subprocess.PIPE,
298
+ stderr=subprocess.PIPE,
299
+ text=True,
300
+ encoding="utf-8",
301
+ errors="replace",
302
+ )
303
+ except FileNotFoundError:
304
+ return {
305
+ "command": command[0],
306
+ "available": False,
307
+ "exitCode": None,
308
+ "stdout": "",
309
+ "stderr": "command not found",
310
+ "durationMs": round((time.monotonic() - started) * 1000),
311
+ }
312
+
313
+ stdout = ""
314
+ stderr = ""
315
+ while True:
316
+ try:
317
+ stdout, stderr = process.communicate(timeout=poll_interval)
318
+ break
319
+ except subprocess.TimeoutExpired:
320
+ elapsed = time.monotonic() - started
321
+ readiness_fraction = min(elapsed / max(readiness_timeout, 1), 1.0)
322
+ percent = round(start_percent + (end_percent - start_percent) * readiness_fraction)
323
+ progress.update(
324
+ phase,
325
+ step_index,
326
+ message,
327
+ percent=percent,
328
+ elapsed_seconds=max(1, round(elapsed)),
329
+ timeout_seconds=readiness_timeout,
330
+ )
331
+ if elapsed >= timeout:
332
+ process.kill()
333
+ stdout, stderr = process.communicate()
334
+ return {
335
+ "command": command[0],
336
+ "available": True,
337
+ "exitCode": None,
338
+ "stdout": stdout.strip(),
339
+ "stderr": f"timed out after {timeout} seconds",
340
+ "durationMs": round((time.monotonic() - started) * 1000),
341
+ }
342
+
343
+ return {
344
+ "command": command[0],
345
+ "available": True,
346
+ "exitCode": process.returncode,
347
+ "stdout": stdout.strip(),
348
+ "stderr": stderr.strip(),
349
+ "durationMs": round((time.monotonic() - started) * 1000),
350
+ }
351
+
352
+
248
353
  def sha256(path: Path) -> str:
249
354
  digest = hashlib.sha256()
250
355
  with path.open("rb") as stream:
@@ -1276,8 +1381,57 @@ def parse_json_output(result: dict, label: str) -> dict:
1276
1381
  raise OnboardingError(f"{label} did not return JSON") from exc
1277
1382
 
1278
1383
 
1384
+ def write_runtime_start_failure(
1385
+ data_root: Path,
1386
+ evidence_dir: Path,
1387
+ launch: dict,
1388
+ phase: str,
1389
+ error: object,
1390
+ elapsed_ms: int,
1391
+ ) -> dict:
1392
+ logs = []
1393
+ for name in ("stdoutLog", "stderrLog"):
1394
+ value = launch.get(name)
1395
+ path = normalized(value) if isinstance(value, str) and value else None
1396
+ item = {"name": name, "path": str(path) if path else None, "exists": bool(path and path.is_file())}
1397
+ if path and path.is_file():
1398
+ stat = path.stat()
1399
+ item.update({"sizeBytes": stat.st_size, "modifiedAt": dt.datetime.fromtimestamp(stat.st_mtime, dt.timezone.utc).isoformat()})
1400
+ logs.append(item)
1401
+ pid = int(launch.get("pid", 0))
1402
+ failure = {
1403
+ "schemaVersion": "foggy-deepseek-runtime-start-failure/v1",
1404
+ "failedAt": now_utc(),
1405
+ "phase": phase,
1406
+ "message": str(error),
1407
+ "elapsedMs": elapsed_ms,
1408
+ "pid": pid or None,
1409
+ "processRunningBeforeCleanup": process_info(pid)["running"] if pid else False,
1410
+ "runtimeUrl": launch.get("runtimeUrl"),
1411
+ "workDir": launch.get("workDir"),
1412
+ "evidenceDir": str(evidence_dir),
1413
+ "logs": logs,
1414
+ }
1415
+ atomic_json(evidence_dir / "failure.json", failure)
1416
+ atomic_json(data_root / "last-runtime-start-failure.json", failure)
1417
+ return failure
1418
+
1419
+
1279
1420
  def runtime_start_command(args: argparse.Namespace) -> dict:
1421
+ global ACTIVE_PROGRESS
1280
1422
  versions = load_versions()
1423
+ readiness_timeout = args.timeout or int(versions["defaults"]["readinessTimeoutSeconds"])
1424
+ progress = ProgressReporter(
1425
+ getattr(args, "progress_file", None),
1426
+ getattr(args, "operation_id", None),
1427
+ getattr(args, "operation_kind", "runtime-start"),
1428
+ total_steps=6,
1429
+ completion_phase="runtime-complete",
1430
+ completion_message="Foggy Runtime is ready",
1431
+ failure_message="Foggy Runtime startup failed",
1432
+ )
1433
+ ACTIVE_PROGRESS = progress
1434
+ progress.update("runtime-preflight", 0, "Checking Runtime state and prerequisites", percent=3)
1281
1435
  install_root = normalized(args.install_root or default_install_root())
1282
1436
  state = read_install_state(install_root)
1283
1437
  data_root = normalized(args.data_root or state["dataRoot"])
@@ -1296,15 +1450,24 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1296
1450
  base_url = prior.get("runtimeUrl")
1297
1451
  if not isinstance(base_url, str) or not base_url:
1298
1452
  raise OnboardingError("Recorded Runtime does not contain runtimeUrl")
1299
- wait_result = command_result(
1453
+ progress.update("runtime-readiness", 3, "Revalidating the recorded Runtime", percent=30, elapsed_seconds=0, timeout_seconds=readiness_timeout)
1454
+ wait_result = command_result_with_progress(
1300
1455
  [cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "wait-ready",
1301
- "--timeout-seconds", str(args.timeout or versions["defaults"]["readinessTimeoutSeconds"]),
1456
+ "--timeout-seconds", str(readiness_timeout),
1302
1457
  "--interval-seconds", "1"],
1303
- timeout=(args.timeout or versions["defaults"]["readinessTimeoutSeconds"]) + 30,
1458
+ readiness_timeout + 30,
1459
+ progress,
1460
+ phase="runtime-readiness",
1461
+ step_index=3,
1462
+ message="Revalidating the recorded Runtime",
1463
+ readiness_timeout=readiness_timeout,
1464
+ start_percent=30,
1465
+ end_percent=85,
1304
1466
  )
1305
1467
  wait_payload = parse_json_output(wait_result, "wait-ready")
1306
1468
  if wait_payload.get("success") is not True:
1307
1469
  raise OnboardingError("wait-ready returned success=false for recorded Runtime")
1470
+ progress.update("runtime-capabilities", 4, "Verifying Runtime API capabilities", percent=90)
1308
1471
  capabilities = parse_json_output(
1309
1472
  command_result(
1310
1473
  [cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "capabilities"],
@@ -1324,6 +1487,8 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1324
1487
  "schemaVersion": capabilities.get("data", {}).get("schemaVersion"),
1325
1488
  "securityMode": capabilities.get("data", {}).get("securityMode"),
1326
1489
  }
1490
+ progress.finish()
1491
+ ACTIVE_PROGRESS = None
1327
1492
  return {
1328
1493
  "success": True,
1329
1494
  **prior,
@@ -1340,6 +1505,7 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1340
1505
  "productionReady": False,
1341
1506
  }
1342
1507
  existing.unlink()
1508
+ progress.update("runtime-preflight", 1, "Checking port and Runtime workspace", percent=10)
1343
1509
  port = args.port or int(versions["defaults"]["port"])
1344
1510
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
1345
1511
  try:
@@ -1350,6 +1516,7 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1350
1516
  work_dir.mkdir(parents=True, exist_ok=True)
1351
1517
  launcher_dir = normalized(state["launcher"]["path"])
1352
1518
  java_exe = args.java or os.environ.get("JAVA_EXE", "java")
1519
+ progress.update("runtime-launcher", 2, "Starting the Java Runtime through Launcher", percent=20)
1353
1520
  if os.name == "nt":
1354
1521
  shell = shutil.which("pwsh") or shutil.which("powershell")
1355
1522
  if not shell:
@@ -1397,12 +1564,25 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1397
1564
  evidence_dir = data_root / "evidence" / f"runtime-start-{dt.datetime.now().strftime('%Y%m%d-%H%M%S')}"
1398
1565
  evidence_dir.mkdir(parents=True, exist_ok=True)
1399
1566
  atomic_json(evidence_dir / "launch.json", launch)
1567
+ startup_started = time.monotonic()
1400
1568
  try:
1401
- wait_result = command_result([cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "wait-ready", "--timeout-seconds", str(args.timeout or versions["defaults"]["readinessTimeoutSeconds"]), "--interval-seconds", "1"], timeout=(args.timeout or versions["defaults"]["readinessTimeoutSeconds"]) + 30)
1569
+ progress.update("runtime-readiness", 3, "Waiting for Java Runtime readiness", percent=30, elapsed_seconds=0, timeout_seconds=readiness_timeout)
1570
+ wait_result = command_result_with_progress(
1571
+ [cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "wait-ready", "--timeout-seconds", str(readiness_timeout), "--interval-seconds", "1"],
1572
+ readiness_timeout + 30,
1573
+ progress,
1574
+ phase="runtime-readiness",
1575
+ step_index=3,
1576
+ message="Waiting for Java Runtime readiness",
1577
+ readiness_timeout=readiness_timeout,
1578
+ start_percent=30,
1579
+ end_percent=85,
1580
+ )
1402
1581
  wait_payload = parse_json_output(wait_result, "wait-ready")
1403
1582
  if wait_payload.get("success") is not True:
1404
1583
  raise OnboardingError("wait-ready returned success=false")
1405
1584
  atomic_json(evidence_dir / "wait-ready.json", wait_payload)
1585
+ progress.update("runtime-capabilities", 4, "Verifying Runtime API capabilities", percent=90)
1406
1586
  capabilities_result = command_result([cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "capabilities"], timeout=30)
1407
1587
  capabilities = parse_json_output(capabilities_result, "capabilities")
1408
1588
  expected_contract = versions["components"]["launcher"]["runtimeApiContract"]
@@ -1411,12 +1591,21 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1411
1591
  if capabilities.get("data", {}).get("securityMode") != versions["defaults"]["securityMode"]:
1412
1592
  raise OnboardingError("Packaged Launcher did not report the expected dev/test security mode")
1413
1593
  atomic_json(evidence_dir / "capabilities.json", capabilities)
1414
- except Exception:
1594
+ except Exception as exc:
1595
+ failure = write_runtime_start_failure(
1596
+ data_root,
1597
+ evidence_dir,
1598
+ launch,
1599
+ progress.phase,
1600
+ exc,
1601
+ round((time.monotonic() - startup_started) * 1000),
1602
+ )
1415
1603
  try:
1416
1604
  os.kill(pid, signal.SIGTERM)
1417
1605
  except OSError:
1418
1606
  pass
1419
- raise
1607
+ raise OnboardingError(f"{exc}; startup diagnostics: {failure['evidenceDir']}") from exc
1608
+ progress.update("runtime-state", 5, "Saving verified Runtime state", percent=96, current_file="runtime-state.json")
1420
1609
  runtime_state = {
1421
1610
  "schemaVersion": RUNTIME_STATE_SCHEMA,
1422
1611
  "startedAt": now_utc(),
@@ -1435,6 +1624,9 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1435
1624
  },
1436
1625
  }
1437
1626
  atomic_json(data_root / "runtime-state.json", runtime_state)
1627
+ (data_root / "last-runtime-start-failure.json").unlink(missing_ok=True)
1628
+ progress.finish()
1629
+ ACTIVE_PROGRESS = None
1438
1630
  return {"success": True, **runtime_state, "productionReady": False}
1439
1631
 
1440
1632
 
@@ -2855,6 +3047,9 @@ def build_parser() -> argparse.ArgumentParser:
2855
3047
  start.add_argument("--namespace")
2856
3048
  start.add_argument("--timeout", type=int)
2857
3049
  start.add_argument("--java")
3050
+ start.add_argument("--progress-file")
3051
+ start.add_argument("--operation-id")
3052
+ start.add_argument("--operation-kind", default="runtime-start")
2858
3053
  start.set_defaults(handler=runtime_start_command)
2859
3054
 
2860
3055
  stop = sub.add_parser("runtime-stop")