@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 +16 -1
- package/experience/linux/prepare.sh +1 -1
- package/lib/client.js +80 -4
- package/lib/index.js +95 -5
- package/package.json +1 -1
- package/skills/foggy-deepseek-onboarding/SKILL.md +21 -3
- package/skills/foggy-deepseek-onboarding/assets/versions.json +1 -1
- package/skills/foggy-deepseek-onboarding/scripts/onboarding.py +438 -35
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.
|
|
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
|
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
|
|
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: '
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
225
|
+
projectRoot,
|
|
226
|
+
contextPath,
|
|
168
227
|
components,
|
|
169
|
-
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
|
|
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
|
@@ -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.
|
|
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
|
|
48
|
-
|
|
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`.
|
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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"],
|
|
1020
|
+
downloads / "skill" / zip_asset["file"], project_root, components["analysisSkill"]["version"],
|
|
1021
|
+
zip_asset["sha256"], versions["packageVersion"], args.replace_skill,
|
|
655
1022
|
)
|
|
656
|
-
|
|
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
|
-
|
|
1861
|
-
|
|
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
|
|
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":
|
|
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
|
|