@fx-data/ai-diagnostics 0.1.20261026070913 → 0.1.20261107142011

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.
Files changed (254) hide show
  1. package/AGENT_SETUP.md +9 -6
  2. package/README.md +5 -5
  3. package/bin/core-ai.mjs +241 -25
  4. package/content/AGENTS.md +37 -57
  5. package/content/README.md +10 -41
  6. package/content/docs/architecture.md +45 -74
  7. package/content/docs/diagnostic-feedback.md +20 -38
  8. package/content/docs/diagnostic-report.md +19 -46
  9. package/content/docs/index.md +40 -128
  10. package/content/docs/scenario-guide.md +34 -52
  11. package/content/knowledge/README.md +49 -0
  12. package/content/knowledge/cases/README.md +60 -0
  13. package/content/knowledge/cases/dashboard-parameter-deep-binding.md +32 -0
  14. package/content/knowledge/common/README.md +31 -0
  15. package/content/knowledge/common/concepts.md +49 -0
  16. package/content/knowledge/common/{knowledge-applicability.md → conclusions.md} +10 -6
  17. package/content/knowledge/common/queries.md +98 -0
  18. package/content/knowledge/common/signals.md +58 -0
  19. package/content/knowledge/common/topics/baseline-comparison.md +23 -0
  20. package/content/knowledge/common/topics/large-results.md +12 -0
  21. package/content/knowledge/common/topics/linked-traces.md +11 -0
  22. package/content/knowledge/common/topics/query-budget.md +18 -0
  23. package/content/knowledge/common/topics/query-example.md +69 -0
  24. package/content/knowledge/common/topics/setup-signoz-mcp.md +102 -0
  25. package/content/knowledge/common/topics/start-and-anchors.md +19 -0
  26. package/content/knowledge/common/workflow.md +22 -0
  27. package/content/knowledge/component/README.md +26 -0
  28. package/content/knowledge/component/concepts.md +22 -0
  29. package/content/knowledge/component/conclusions.md +11 -0
  30. package/content/knowledge/component/queries.md +7 -0
  31. package/content/knowledge/component/signals.md +75 -0
  32. package/content/knowledge/component/workflow.md +23 -0
  33. package/content/knowledge/dashboard/README.md +31 -0
  34. package/content/knowledge/dashboard/concepts.md +26 -0
  35. package/content/knowledge/dashboard/conclusions.md +21 -0
  36. package/content/knowledge/dashboard/queries.md +69 -0
  37. package/content/knowledge/dashboard/signals.md +76 -0
  38. package/content/knowledge/dashboard/topics/component-comparison.md +9 -0
  39. package/content/knowledge/dashboard/topics/dynamic-fields.md +21 -0
  40. package/content/knowledge/dashboard/topics/filter-tree.md +35 -0
  41. package/content/knowledge/dashboard/topics/name-binding.md +9 -0
  42. package/content/knowledge/dashboard/workflow.md +35 -0
  43. package/content/knowledge/dataset/README.md +27 -0
  44. package/content/knowledge/dataset/concepts.md +30 -0
  45. package/content/knowledge/dataset/conclusions.md +11 -0
  46. package/content/knowledge/dataset/queries.md +7 -0
  47. package/content/knowledge/dataset/signals.md +61 -0
  48. package/content/knowledge/dataset/topics/steps.md +59 -0
  49. package/content/knowledge/dataset/workflow.md +27 -0
  50. package/content/knowledge/engine/README.md +27 -0
  51. package/content/knowledge/engine/concepts.md +80 -0
  52. package/content/knowledge/engine/conclusions.md +49 -0
  53. package/content/knowledge/engine/queries.md +117 -0
  54. package/content/knowledge/engine/signals.md +73 -0
  55. package/content/knowledge/engine/topics/metric-fields.md +104 -0
  56. package/content/knowledge/engine/topics/patterns.md +50 -0
  57. package/content/knowledge/engine/workflow.md +20 -0
  58. package/content/knowledge/signal-index.md +117 -0
  59. package/content/knowledge/update/README.md +39 -0
  60. package/content/knowledge/update/concepts.md +93 -0
  61. package/content/knowledge/update/conclusions.md +59 -0
  62. package/content/knowledge/update/queries.md +103 -0
  63. package/content/knowledge/update/signals.md +57 -0
  64. package/content/knowledge/update/topics/correlations.md +63 -0
  65. package/content/knowledge/update/topics/discover-executions.md +31 -0
  66. package/content/knowledge/update/topics/events-execution.md +136 -0
  67. package/content/knowledge/update/topics/events-intake.md +102 -0
  68. package/content/knowledge/update/topics/events-outcome.md +163 -0
  69. package/content/knowledge/update/topics/lifecycle.md +58 -0
  70. package/content/knowledge/update/topics/scene-producers.md +258 -0
  71. package/content/knowledge/update/workflow.md +29 -0
  72. package/content-manifest.json +73 -192
  73. package/package.json +2 -2
  74. package/content/.agents/skills/analyze-analysis-table-update-runtime/SKILL.md +0 -35
  75. package/content/.agents/skills/analyze-analysis-table-update-runtime/agents/openai.yaml +0 -7
  76. package/content/.agents/skills/analyze-analysis-table-update-runtime/references/discover-update-executions.md +0 -34
  77. package/content/.agents/skills/analyze-analysis-table-update-runtime/references/update-diagnostic-contract.md +0 -101
  78. package/content/.agents/skills/analyze-engine-metric/SKILL.md +0 -76
  79. package/content/.agents/skills/analyze-engine-metric/agents/openai.yaml +0 -13
  80. package/content/.agents/skills/analyze-engine-metric/scripts/analyze_engine_metric.py +0 -270
  81. package/content/.agents/skills/analyze-engine-metric/scripts/engine_metric_analysis/__init__.py +0 -11
  82. package/content/.agents/skills/analyze-engine-metric/scripts/engine_metric_analysis/metric_analyzer.py +0 -791
  83. package/content/.agents/skills/analyze-engine-metric/scripts/engine_metric_analysis/signoz_extract.py +0 -278
  84. package/content/.agents/skills/diagnose-component-performance/SKILL.md +0 -52
  85. package/content/.agents/skills/diagnose-component-performance/agents/openai.yaml +0 -7
  86. package/content/.agents/skills/diagnose-component-performance/references/experience.md +0 -34
  87. package/content/.agents/skills/diagnose-dashboard-performance/SKILL.md +0 -92
  88. package/content/.agents/skills/diagnose-dashboard-performance/agents/openai.yaml +0 -7
  89. package/content/.agents/skills/diagnose-dashboard-performance/references/experience.md +0 -63
  90. package/content/.agents/skills/diagnose-dataset-performance/SKILL.md +0 -52
  91. package/content/.agents/skills/diagnose-dataset-performance/agents/openai.yaml +0 -7
  92. package/content/.agents/skills/diagnose-dataset-performance/references/experience.md +0 -33
  93. package/content/.agents/skills/diagnose-performance/SKILL.md +0 -77
  94. package/content/.agents/skills/diagnose-performance/agents/openai.yaml +0 -7
  95. package/content/.agents/skills/diagnose-update-performance/SKILL.md +0 -55
  96. package/content/.agents/skills/diagnose-update-performance/agents/openai.yaml +0 -7
  97. package/content/.agents/skills/diagnose-update-performance/references/experience.md +0 -47
  98. package/content/.agents/skills/setup-signoz-mcp/SKILL.md +0 -30
  99. package/content/.agents/skills/setup-signoz-mcp/agents/openai.yaml +0 -7
  100. package/content/.agents/skills/setup-signoz-mcp/references/installation.md +0 -75
  101. package/content/.agents/skills/setup-signoz-mcp/references/windows-artifact.md +0 -14
  102. package/content/docs/npm-cli.md +0 -202
  103. package/content/docs/roadmap.md +0 -44
  104. package/content/docs/skill-writing-guide.md +0 -65
  105. package/content/knowledge/common/signoz-environments.json +0 -52
  106. package/content/knowledge/common/signoz-environments.md +0 -55
  107. package/content/knowledge/common/signoz-query-example.md +0 -41
  108. package/content/knowledge/common/signoz-query-policy.md +0 -145
  109. package/content/knowledge/common/trace-routing.md +0 -63
  110. package/content/knowledge/component/observability.md +0 -60
  111. package/content/knowledge/dashboard/observability.md +0 -168
  112. package/content/knowledge/dashboard/query-examples.md +0 -63
  113. package/content/knowledge/dataset/observability.md +0 -68
  114. package/content/knowledge/engine/metric-artifacts.md +0 -25
  115. package/content/knowledge/engine/metric-retrieval.md +0 -107
  116. package/content/knowledge/engine/metric-semantics.md +0 -172
  117. package/content/knowledge/engine/performance-patterns.md +0 -86
  118. package/content/knowledge/update/diagnostic/manifest.json +0 -209
  119. package/content/knowledge/update/diagnostic/observability/edges/edge-attempt-to-capacity-deferral.json +0 -17
  120. package/content/knowledge/update/diagnostic/observability/edges/edge-attempt-to-lock-requeue.json +0 -18
  121. package/content/knowledge/update/diagnostic/observability/edges/edge-attempt-to-merge.json +0 -17
  122. package/content/knowledge/update/diagnostic/observability/edges/edge-candidate-to-effective-root.json +0 -16
  123. package/content/knowledge/update/diagnostic/observability/edges/edge-capacity-deferral-to-attempt.json +0 -16
  124. package/content/knowledge/update/diagnostic/observability/edges/edge-chain-node-to-execution-detail.json +0 -18
  125. package/content/knowledge/update/diagnostic/observability/edges/edge-dlx-recovery-to-attempt.json +0 -16
  126. package/content/knowledge/update/diagnostic/observability/edges/edge-effective-execution-to-listener-before.json +0 -21
  127. package/content/knowledge/update/diagnostic/observability/edges/edge-effective-root-to-chain.json +0 -17
  128. package/content/knowledge/update/diagnostic/observability/edges/edge-engine-task-to-primary-result.json +0 -17
  129. package/content/knowledge/update/diagnostic/observability/edges/edge-execution-detail-to-publication.json +0 -16
  130. package/content/knowledge/update/diagnostic/observability/edges/edge-init-recovery-to-publication.json +0 -16
  131. package/content/knowledge/update/diagnostic/observability/edges/edge-listener-complete-to-child-release.json +0 -21
  132. package/content/knowledge/update/diagnostic/observability/edges/edge-listener-to-derived-chain.json +0 -21
  133. package/content/knowledge/update/diagnostic/observability/edges/edge-load-decision-to-load-operation.json +0 -16
  134. package/content/knowledge/update/diagnostic/observability/edges/edge-load-operation-to-engine-task.json +0 -17
  135. package/content/knowledge/update/diagnostic/observability/edges/edge-lock-requeue-to-attempt.json +0 -16
  136. package/content/knowledge/update/diagnostic/observability/edges/edge-logical-parent-to-child.json +0 -18
  137. package/content/knowledge/update/diagnostic/observability/edges/edge-merge-member-to-effective-execution.json +0 -18
  138. package/content/knowledge/update/diagnostic/observability/edges/edge-merge-to-load-decision.json +0 -17
  139. package/content/knowledge/update/diagnostic/observability/edges/edge-no-load-to-primary-result.json +0 -16
  140. package/content/knowledge/update/diagnostic/observability/edges/edge-primary-result-to-child-frontier.json +0 -18
  141. package/content/knowledge/update/diagnostic/observability/edges/edge-primary-result-to-listener.json +0 -23
  142. package/content/knowledge/update/diagnostic/observability/edges/edge-publication-to-attempt.json +0 -16
  143. package/content/knowledge/update/diagnostic/observability/edges/edge-ready-recovery-to-publication.json +0 -16
  144. package/content/knowledge/update/diagnostic/observability/edges/edge-request-to-candidate-root.json +0 -16
  145. package/content/knowledge/update/diagnostic/observability/identity-units/candidate-root.json +0 -13
  146. package/content/knowledge/update/diagnostic/observability/identity-units/dag-node.json +0 -14
  147. package/content/knowledge/update/diagnostic/observability/identity-units/effective-root.json +0 -14
  148. package/content/knowledge/update/diagnostic/observability/identity-units/engine-task.json +0 -13
  149. package/content/knowledge/update/diagnostic/observability/identity-units/execution-attempt.json +0 -12
  150. package/content/knowledge/update/diagnostic/observability/identity-units/execution-detail.json +0 -13
  151. package/content/knowledge/update/diagnostic/observability/identity-units/listener-invocation.json +0 -11
  152. package/content/knowledge/update/diagnostic/observability/identity-units/load-decision.json +0 -12
  153. package/content/knowledge/update/diagnostic/observability/identity-units/load-operation.json +0 -11
  154. package/content/knowledge/update/diagnostic/observability/identity-units/logical-dag-edge.json +0 -11
  155. package/content/knowledge/update/diagnostic/observability/identity-units/merge-decision.json +0 -12
  156. package/content/knowledge/update/diagnostic/observability/identity-units/node-primary-result.json +0 -12
  157. package/content/knowledge/update/diagnostic/observability/identity-units/queue-publication.json +0 -11
  158. package/content/knowledge/update/diagnostic/observability/identity-units/requeue.json +0 -11
  159. package/content/knowledge/update/diagnostic/observability/identity-units/update-chain.json +0 -13
  160. package/content/knowledge/update/diagnostic/observability/identity-units/update-request.json +0 -12
  161. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-after-commit-delivery.json +0 -131
  162. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-attempt-consumed.json +0 -175
  163. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-candidate-root-filter.json +0 -119
  164. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-capacity-deferred.json +0 -121
  165. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-child-release.json +0 -146
  166. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-dlx-recovery.json +0 -100
  167. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-effective-root-selection.json +0 -111
  168. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-engine-handoff.json +0 -171
  169. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-execution-frontier-persisted.json +0 -115
  170. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-init-recovery.json +0 -93
  171. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-intent-accepted.json +0 -151
  172. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-listener-derived-chain.json +0 -112
  173. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-listener-invocation.json +0 -154
  174. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-load-decision.json +0 -155
  175. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-load-operation.json +0 -108
  176. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-lock-requeued.json +0 -108
  177. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-logical-dependency-expansion.json +0 -129
  178. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-merge-decision.json +0 -174
  179. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-primary-node-result.json +0 -318
  180. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-ready-recovery.json +0 -92
  181. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-business-attempt.json +0 -33
  182. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-capacity-cycle.json +0 -31
  183. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-child-release.json +0 -32
  184. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-commit-publication.json +0 -29
  185. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-control-planning.json +0 -30
  186. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-engine-task.json +0 -27
  187. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-listener-callback.json +0 -27
  188. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-load-operation.json +0 -27
  189. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-lock-cycle.json +0 -31
  190. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-main-queue-wait.json +0 -32
  191. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-merge-evaluation.json +0 -29
  192. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-parent-dependency-wait.json +0 -31
  193. package/content/knowledge/update/diagnostic/observability-index.json +0 -558
  194. package/content/knowledge/update/diagnostic/recipe-index.json +0 -85
  195. package/content/knowledge/update/diagnostic/recipes/dag-chain.json +0 -676
  196. package/content/knowledge/update/diagnostic/recipes/performance-window.json +0 -1128
  197. package/content/knowledge/update/diagnostic/recipes/scene-origin-async-message-or-callback.json +0 -438
  198. package/content/knowledge/update/diagnostic/recipes/scene-origin-derived-update-chain.json +0 -510
  199. package/content/knowledge/update/diagnostic/recipes/scene-origin-direct-service-call.json +0 -438
  200. package/content/knowledge/update/diagnostic/recipes/scene-origin-http.json +0 -438
  201. package/content/knowledge/update/diagnostic/recipes/scene-origin-internal-event.json +0 -438
  202. package/content/knowledge/update/diagnostic/recipes/scene-origin-job.json +0 -438
  203. package/content/knowledge/update/diagnostic/recipes/scene-origin-recovery-or-migration.json +0 -438
  204. package/content/knowledge/update/diagnostic/recipes/table-history.json +0 -508
  205. package/content/knowledge/update/diagnostic/recipes/table-in-chain.json +0 -595
  206. package/content/knowledge/update/diagnostic/scene-index.json +0 -252
  207. package/content/knowledge/update/diagnostic/scenes/active.json +0 -47
  208. package/content/knowledge/update/diagnostic/scenes/add.json +0 -140
  209. package/content/knowledge/update/diagnostic/scenes/agent.json +0 -20
  210. package/content/knowledge/update/diagnostic/scenes/archive.json +0 -32
  211. package/content/knowledge/update/diagnostic/scenes/auth-change.json +0 -116
  212. package/content/knowledge/update/diagnostic/scenes/call-back.json +0 -32
  213. package/content/knowledge/update/diagnostic/scenes/case-install.json +0 -20
  214. package/content/knowledge/update/diagnostic/scenes/case-replace.json +0 -7
  215. package/content/knowledge/update/diagnostic/scenes/cdc-schedule.json +0 -20
  216. package/content/knowledge/update/diagnostic/scenes/compute-model.json +0 -20
  217. package/content/knowledge/update/diagnostic/scenes/copy-data.json +0 -32
  218. package/content/knowledge/update/diagnostic/scenes/corp-linked.json +0 -7
  219. package/content/knowledge/update/diagnostic/scenes/corp-member-add.json +0 -20
  220. package/content/knowledge/update/diagnostic/scenes/corp-member-del.json +0 -7
  221. package/content/knowledge/update/diagnostic/scenes/corp-member-modify.json +0 -32
  222. package/content/knowledge/update/diagnostic/scenes/corp-member-sync.json +0 -152
  223. package/content/knowledge/update/diagnostic/scenes/corp-setting-member-rename.json +0 -152
  224. package/content/knowledge/update/diagnostic/scenes/corp-setting.json +0 -20
  225. package/content/knowledge/update/diagnostic/scenes/cross-project-allow.json +0 -32
  226. package/content/knowledge/update/diagnostic/scenes/default.json +0 -32
  227. package/content/knowledge/update/diagnostic/scenes/del.json +0 -20
  228. package/content/knowledge/update/diagnostic/scenes/dynamic-time.json +0 -20
  229. package/content/knowledge/update/diagnostic/scenes/edit.json +0 -92
  230. package/content/knowledge/update/diagnostic/scenes/excel-append.json +0 -44
  231. package/content/knowledge/update/diagnostic/scenes/excel-save.json +0 -32
  232. package/content/knowledge/update/diagnostic/scenes/folder-copy.json +0 -32
  233. package/content/knowledge/update/diagnostic/scenes/global.json +0 -56
  234. package/content/knowledge/update/diagnostic/scenes/handover.json +0 -32
  235. package/content/knowledge/update/diagnostic/scenes/history-handle.json +0 -20
  236. package/content/knowledge/update/diagnostic/scenes/manual-update.json +0 -44
  237. package/content/knowledge/update/diagnostic/scenes/migration.json +0 -20
  238. package/content/knowledge/update/diagnostic/scenes/param-change.json +0 -20
  239. package/content/knowledge/update/diagnostic/scenes/polars-error.json +0 -32
  240. package/content/knowledge/update/diagnostic/scenes/preview-exception.json +0 -20
  241. package/content/knowledge/update/diagnostic/scenes/preview-invalid.json +0 -20
  242. package/content/knowledge/update/diagnostic/scenes/project-excel-move.json +0 -20
  243. package/content/knowledge/update/diagnostic/scenes/re-check.json +0 -7
  244. package/content/knowledge/update/diagnostic/scenes/replace.json +0 -20
  245. package/content/knowledge/update/diagnostic/scenes/rollback.json +0 -68
  246. package/content/knowledge/update/diagnostic/scenes/save-as.json +0 -44
  247. package/content/knowledge/update/diagnostic/scenes/schedu.json +0 -56
  248. package/content/knowledge/update/diagnostic/scenes/table-move-other-workspace.json +0 -20
  249. package/content/knowledge/update/diagnostic/scenes/user-holder-to-normal.json +0 -20
  250. package/content/knowledge/update/observability.md +0 -16
  251. package/content/knowledge/update/runtime-evidence.md +0 -32
  252. package/content/scripts/receive_tool_result.py +0 -57
  253. /package/content/{.agents/skills/setup-signoz-mcp/scripts → scripts}/install_signoz_mcp.ps1 +0 -0
  254. /package/content/{.agents/skills/setup-signoz-mcp/scripts → scripts}/install_signoz_mcp.py +0 -0
package/AGENT_SETUP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FX 数据诊断:Agent 安装与管理
2
2
 
3
- 本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261026070913`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
3
+ 本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261107142011`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
4
4
 
5
5
  用户明确要求安装、更新、回退、卸载或检查 FX 数据诊断时,也会触发薄 Skill,并直接执行对应管理命令,不先运行 `context` 或读取诊断规则;只有开始诊断时才按薄 Skill 加载内容。普通聊天不主动加载。
6
6
 
@@ -49,20 +49,22 @@
49
49
  - 独立 Node 环境下,用其绝对路径运行配套 `npm-cli.js`(Unix:`lib/node_modules/npm/bin/npm-cli.js`;Windows:`node_modules/npm/bin/npm-cli.js`)。通过同一环境的 `npm root -g` 找到包,沿用自定义 prefix,再用同一 Node 调用包内 `bin/core-ai.mjs` 登记。路径含空格时保留引号;Windows PowerShell 调用带引号的可执行路径需要 `&`。不依赖用户项目目录。
50
50
  - 全局目录不可写时仅为本次安装指定用户目录的 `--prefix`;现有源找不到本包时仅为本次命令指定 `--registry=https://registry.npmjs.org`,不改全局 npm 配置。
51
51
  - 安装失败时同时读取 stderr 中的错误和分项检查。Node 路径失效时修复原运行环境;内容损坏、所有权冲突或权限错误应处理具体错误,不通过第二份安装绕过。需要进一步排错时再运行 `doctor`。
52
- - 复用旧版时,若 `install` 尚未返回 `checks` 和 `context`,仅此分支补跑稳定入口的 `doctor`、`context` 验证,不因此升级或重新下载。Python 缺失不阻止登记,首次解析材料时再准备。
52
+ - 复用旧版时,若 `install` 尚未返回 `checks` 和 `context`,仅此分支补跑稳定入口的 `doctor`、`context` 验证,不因此升级或重新下载。Python 缺失不阻止登记,首次运行包内 Python 脚本时再准备。
53
53
  - WorkBuddy 的 `.workbuddy/skills` 与 `.workbuddy-ai/skills` 由 CLI 按本机线索选择;无法确认时按 CLI 提示处理,不自行扫描客户端缓存或改成两处同时登记。
54
54
 
55
55
  ## 按需使用
56
56
 
57
- 用户明确要求用 FX 数据诊断分析问题后,按薄 Skill 执行 `context`,读取返回的 `agent_rules`,只加载当前问题需要的 Skill 和引用。固定本次调查的版本与 `pinned_cli`;普通聊天和管理请求无需预加载诊断内容。
57
+ 用户明确要求用 FX 数据诊断分析问题后,按薄 Skill 调用一次稳定入口的 `context`,读取返回的 `agent_rules`,只加载当前问题需要的 Skill 和引用。CLI 同时在后台检查更新,固定本次调查返回的版本与 `pinned_cli`;同一调查继续复用,不重复调用稳定入口的 `context`。普通聊天和管理请求无需预加载诊断内容或检查更新。
58
58
 
59
- 已有 SigNoZ MCP 直接复用;实时查询受阻时才按包内配置 Skill 处理,凭据只保存在本机。Python 解析按需准备;独立解释器可通过 `FX_DATA_DIAGNOSTICS_PYTHON` 指定绝对路径。
59
+ 已有 SigNoZ MCP 直接复用;实时查询受阻时才按包内配置 Skill 处理,凭据只保存在本机。Python 按需准备;独立解释器可通过 `FX_DATA_DIAGNOSTICS_PYTHON` 指定绝对路径。
60
60
 
61
61
  ## 更新与回退
62
62
 
63
- 用户要求更新时,调用稳定入口的 `update`;要求回退到上次更新前时调用 `rollback`,明确指定版本时调用 `update --version <版本号>`。新调查使用切换后的版本,进行中的调查保持原版本。新增客户端只登记,不隐式更新。
63
+ 用户要求更新时,调用稳定入口的 `update`;要求回退到上次更新前时调用 `rollback`,明确指定版本时调用 `update --version <版本号>`。新调查使用切换后的版本,进行中的调查保持原版本。回退不锁定后续版本,下一次新诊断仍检查最新版;需要持续使用旧版时按配置变量关闭后台更新。新增客户端只登记,不隐式更新。
64
64
 
65
- 用户要求自动更新时,通过当前客户端或系统调度同一稳定入口的 `update`,告知是否设置成功;普通诊断不自动检查或更新。版本切换后按客户端要求刷新技能或新开聊天。
65
+ 使用时的后台更新由 CLI 完成,无需 Agent 设置定时任务或另跑 `update`。通知读取、去重和提示按 [安装副本的助手规则](content/AGENTS.md#安装副本的助手规则)执行,不轮询等待。
66
+
67
+ 旧版用户需先手动升级一次才能获得后台更新能力。版本切换后按客户端要求刷新技能或新开聊天。
66
68
 
67
69
  若旧试用版提示需要迁移,先用当前 npm 包替换全局程序,直接调用新包 CLI 的 `update`,再登记与检查。迁移保留客户端登记、诊断材料和独立 MCP 配置;旧试用版不作为回退目标。
68
70
 
@@ -83,5 +85,6 @@ CLI 使用首次安装记录的 Node/npm 和 prefix,在无网络模式下清
83
85
  | `FX_DATA_DIAGNOSTICS_HOME` | 自定义共享安装目录 |
84
86
  | `FX_DATA_DIAGNOSTICS_PYTHON` | 自定义 Python 可执行文件 |
85
87
  | `FX_DATA_DIAGNOSTICS_AGENT_HOME` | 自定义客户端用户目录,通常无需设置 |
88
+ | `FX_DATA_DIAGNOSTICS_AUTO_UPDATE` | 默认开启后台更新;设为 `0` 可关闭,适用于受限环境或需要持续使用旧版时 |
86
89
 
87
90
  显式 CLI 参数优先,其次是环境变量,未指定时使用默认目录或自动发现的运行环境。
package/README.md CHANGED
@@ -22,13 +22,13 @@
22
22
 
23
23
  ## 更新
24
24
 
25
- > 更新 FX 数据诊断。
25
+ 每次开始新诊断时,工具会在后台检查更新,不等待更新即可排查。更新成功后,各 Agent 的下一次新诊断使用新版;当前调查保持原版本,更新失败也不影响排查。Agent 会随后续使用提示更新状态,没有新版时不打扰你。
26
26
 
27
- 已启用的 Agent 共用更新后的版本。需要自动更新时,可以说:
27
+ 需要立即更新并等待完成时说:
28
28
 
29
- > 帮我设置每天上午 9 点(北京时间)检查并更新 FX 数据诊断的任务,失败时通知我。
29
+ > 更新 FX 数据诊断。
30
30
 
31
- 能否设置定时任务取决于当前Agent。若新版有问题,可以说“把 FX 数据诊断回退到这次更新前的版本”。
31
+ 旧版用户先手动更新一次,即可获得后台更新能力。若新版有问题,可以说“把 FX 数据诊断回退到这次更新前的版本”;需要持续使用旧版时,同时让 Agent 关闭后台更新。
32
32
 
33
33
  ## 卸载
34
34
 
@@ -38,4 +38,4 @@
38
38
 
39
39
  ---
40
40
 
41
- **给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261026070913/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
41
+ **给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261107142011/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
package/bin/core-ai.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // Distribution only: diagnostic rules and parsers live in the unchanged content bundle.
2
+ // Distribution only: diagnostic rules and scripts live in the unchanged content bundle.
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { createHash, randomUUID } from 'node:crypto';
7
- import { spawnSync } from 'node:child_process';
7
+ import { spawn, spawnSync } from 'node:child_process';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
 
10
10
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -29,7 +29,7 @@ function argumentsFor(argv) {
29
29
  if (args[i] === '--activate-package') {
30
30
  if (command !== 'update') throw new Error('--activate-package is reserved for the update subprocess');
31
31
  options.activatePackage = true;
32
- } else if (['--home', '--agent-home', '--agent', '--skills-dir', '--registry', '--version'].includes(args[i])) {
32
+ } else if (['--home', '--agent-home', '--agent', '--skills-dir', '--registry', '--version', '--background-job'].includes(args[i])) {
33
33
  if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`Missing value for ${args[i]}`);
34
34
  options[args[i].slice(2)] = args[++i];
35
35
  } else rest.push(args[i]);
@@ -53,11 +53,11 @@ function commandHelp(argv) {
53
53
  let target;
54
54
  if (['help', '--help', '-h'].includes(command)) target = Object.hasOwn(commands, args[0]) ? args[0] : 'help';
55
55
  else if (Object.hasOwn(commands, command) && args.some(arg => arg === '--help' || arg === '-h')) target = command;
56
- else return null; // metric/python keep their parser or script help arguments.
56
+ else return null; // python keeps the script's own help arguments.
57
57
  const note = '帮助只显示说明,不执行命令。安装和管理成功以命令返回结果为准,简短反馈后停止,无需额外 status 或 doctor。';
58
58
  if (target !== 'help') return { ok: true, action: 'help', command: target, ...commands[target], note };
59
59
  return { ok: true, action: 'help', name: commandName, display_name: 'FX 数据诊断',
60
- commands: [...Object.values(commands).map(value => value.usage), 'metric <parser options>', 'python <script> [args...]'],
60
+ commands: [...Object.values(commands).map(value => value.usage), 'python <script> [args...]'],
61
61
  registry: defaultRegistry, note };
62
62
  }
63
63
 
@@ -183,9 +183,13 @@ ${powershell} context
183
183
 
184
184
  后续管理命令使用同一组 Node 和稳定脚本路径,将 context 替换为对应子命令,不依赖 PATH 中的全局 CLI 版本。
185
185
 
186
- context 返回当前版本、内容路径和 agent_rules。读取 agent_rules,按问题只读取所需 Skill 和引用,不预加载全部知识,也不切换用户工作区。包内仓库相对路径以 content_root 为基准,文档链接相对引用文件解析。
186
+ 每次新诊断只调用一次稳定入口 context:立即取得并固定本次版本,同时后台检查更新,不等待网络。同一调查继续复用首次结果,不重复调用稳定入口 context;需要重新读取固定路径时使用 pinned_cli context,不触发新检查。
187
187
 
188
- 当前调查固定使用首次 context 返回的 node、pinned_cli 和内容版本。脚本通过 <node> <pinned_cli> python <脚本绝对路径> ... 执行;Windows PowerShell 同样在带引号的 Node 路径前加 &。Metric 调用先读取 analyze-engine-metric 的输入表:纯文本用 metric --metric;SigNoZ JSON 用 metric --metric-log-json <文件> --task-name <选定 task>。通过 session 获取临时目录,日志和产物不写入安装内容。只有解析脚本需要 Python;doctor 的 python.available=false 表示按需补齐,不代表安装失败。已有 MCP 直接复用;实时查询受阻时才读取 setup-signoz-mcp。
188
+ context 返回当前版本、内容路径和 agent_rules。读取 agent_rules,按问题只读取所需知识(先看对应域的 workflow.md),不预加载全部知识,也不切换用户工作区。包内仓库相对路径以 content_root 为基准,文档链接相对引用文件解析。
189
+
190
+ context、status、session 的 auto_update.notification,以及 python 标准错误中 fx_data_diagnostics.auto_update.notification,是更新提示。有提示时按 message 简短告知用户;无新版不打扰。不为获取状态额外调用 CLI 或轮询;本轮没有后续 CLI 调用时,下次使用再补报。更新成功只影响下次新诊断,失败继续使用本次版本。
191
+
192
+ 当前调查固定使用首次 context 返回的 node、pinned_cli 和内容版本。脚本通过 <node> <pinned_cli> python <脚本绝对路径> ... 执行;Windows PowerShell 同样在带引号的 Node 路径前加 &。通过 session 获取临时目录,保存日志和 Metric 正文,不写入安装内容。只有包内 Python 脚本需要 Python;doctor 的 python.available=false 表示按需补齐,不代表安装失败。已有 MCP 直接复用;实时查询受阻时才读取 setup-signoz-mcp。
189
193
 
190
194
  用户要求更新时执行稳定入口 update,默认使用 latest;要求回退到上次更新前时执行 rollback,也可用 --version 指定版本。已保留版本校验通过后直接复用。各客户端的新调查共用切换后的版本,进行中调查继续使用原 pinned_cli。更新后按客户端要求刷新技能或新开聊天;安装副本不做 Git 更新。
191
195
 
@@ -262,7 +266,7 @@ function skillChecks(targets, home, installId) {
262
266
  function contextFor(root, home, pkg, manifest) {
263
267
  const content = path.join(root, 'content');
264
268
  return { version: pkg.version, source_commit: manifest.source_commit, content_root: content,
265
- agent_rules: path.join(content, 'AGENTS.md'), skills_root: path.join(content, '.agents', 'skills'),
269
+ agent_rules: path.join(content, 'AGENTS.md'),
266
270
  pinned_cli: path.join(root, 'bin', 'core-ai.mjs'), node: process.execPath,
267
271
  launcher: launchers(home).executable, launcher_script: launchers(home).script };
268
272
  }
@@ -362,6 +366,7 @@ function install(home, options, { sourceRoot = packageRoot, action = 'install' }
362
366
  const { pkg, manifest } = verifyBundle(sourceRoot, { allowMacMetadata: isInstalledBundle(sourceRoot, home) });
363
367
  noSymlinkAncestors(home);
364
368
  const prior = ownedState(home, false);
369
+ if (options['background-job']) assertBackgroundJob(home, options['background-job'], pkg.version);
365
370
  if (!prior && fs.existsSync(home) && fs.readdirSync(home).length) throw new Error('Refusing to take over a nonempty installation directory');
366
371
  const ownership = prior?.ownership || { schema_version: 1, home, install_id: randomUUID() };
367
372
  const registrationOnly = Boolean(action === 'install' && prior?.state?.lifecycle_protocol === lifecycleProtocol);
@@ -387,9 +392,13 @@ function install(home, options, { sourceRoot = packageRoot, action = 'install' }
387
392
  if (!prior) atomicWrite(path.join(home, homeMarker), JSON.stringify(ownership));
388
393
  const releaseLock = acquireInstallLock(home);
389
394
  let backup = [];
395
+ const launcherBackup = [];
390
396
  const release = path.join(home, 'releases', pkg.version);
391
397
  const stage = `${release}.${randomUUID()}.tmp`;
392
398
  try {
399
+ // A download may outlive its worker or a manual version change. Recheck at
400
+ // activation, under the installation lock, before touching shared entries.
401
+ if (options['background-job']) assertBackgroundJob(home, options['background-job'], pkg.version);
393
402
  backup = writeDirectories.map(target => ({ ...target, existed: fs.existsSync(target.directory), text: fs.existsSync(path.join(target.directory, 'SKILL.md')) ? fs.readFileSync(path.join(target.directory, 'SKILL.md')) : null, owner: fs.existsSync(path.join(target.directory, ownerFile)) ? fs.readFileSync(path.join(target.directory, ownerFile)) : null }));
394
403
  if (fs.existsSync(release)) {
395
404
  verifyBundle(release, { allowMacMetadata: Boolean(prior?.state) });
@@ -412,15 +421,21 @@ function install(home, options, { sourceRoot = packageRoot, action = 'install' }
412
421
  [path.join(home, 'bin', commandName), `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(launchers(home).script)} "$@"\n`],
413
422
  [path.join(home, 'bin', `${commandName}.cmd`), `@echo off\r\n"${process.execPath}" "%~dp0core-ai.mjs" %*\r\n`],
414
423
  ]) {
415
- if (!registrationOnly || !fs.existsSync(file)) atomicWrite(file, text, 0o755);
424
+ if (!registrationOnly || !fs.existsSync(file)) {
425
+ const existed = fs.existsSync(file);
426
+ launcherBackup.push({ file, existed, text: existed ? fs.readFileSync(file) : null,
427
+ mode: existed ? fs.statSync(file).mode & 0o777 : 0o755 });
428
+ atomicWrite(file, text, 0o755);
429
+ }
416
430
  }
417
431
  const registrationReady = performance.now();
418
432
  const checks = { integrity: { status: 'ok' }, skills: skillChecks(targets, home, ownership.install_id), context: { status: 'ok' } };
419
433
  if (checks.skills.status !== 'ok') throw new Error(checks.skills.entries.filter(entry => entry.status === 'error').map(entry => entry.error).join('; '));
420
434
  const context = contextFor(release, home, pkg, manifest);
421
435
  fs.accessSync(context.agent_rules, fs.constants.R_OK);
422
- if (!fs.statSync(context.skills_root).isDirectory()) throw new Error('Diagnostic skills directory is unavailable');
423
- fs.accessSync(context.skills_root, fs.constants.R_OK);
436
+ const knowledgeRoot = path.join(context.content_root, 'knowledge');
437
+ if (!fs.statSync(knowledgeRoot).isDirectory()) throw new Error('Diagnostic knowledge directory is unavailable');
438
+ fs.accessSync(knowledgeRoot, fs.constants.R_OK);
424
439
  if (!registrationOnly) atomicWrite(path.join(home, homeMarker), JSON.stringify(ownership));
425
440
  const releaseProtocols = { ...prior?.state?.release_protocols };
426
441
  if (prior?.state?.lifecycle_protocol === lifecycleProtocol) releaseProtocols[prior.state.version] = lifecycleProtocol;
@@ -430,6 +445,9 @@ function install(home, options, { sourceRoot = packageRoot, action = 'install' }
430
445
  const npmInstallation = prior?.state ? prior.state.npm_installation : detectNpmInstallation(sourceRoot);
431
446
  const state = { schema_version: 1, lifecycle_protocol: lifecycleProtocol, release_protocols: releaseProtocols, install_id: ownership.install_id, package: pkg.name, version: pkg.version, previous_version: prior?.state?.version === pkg.version ? prior.state.previous_version : prior?.state?.version || null, registry, skills: targets, source_commit: manifest.source_commit,
432
447
  ...(npmInstallation ? { npm_installation: npmInstallation } : {}) };
448
+ // Copying and validation can outlast the background deadline. The last
449
+ // check belongs immediately before committing the active version too.
450
+ if (options['background-job']) assertBackgroundJob(home, options['background-job'], pkg.version);
433
451
  if (JSON.stringify(state) !== JSON.stringify(prior?.state)) atomicWrite(path.join(home, 'state.json'), JSON.stringify(state, null, 2));
434
452
  const finished = performance.now();
435
453
  const elapsed = (from, to) => Math.round((to - from) * 100) / 100;
@@ -440,6 +458,10 @@ function install(home, options, { sourceRoot = packageRoot, action = 'install' }
440
458
  agent_hint: '安装登记与文件检查已完成。按返回结果用 2–3 句话反馈版本、首次安装或复用、需要的刷新及使用提示后结束;不再运行 status/ls/doctor/context 或读取源码、客户端缓存。本工具不要求安装清单或长期记忆,仍遵循客户端强制规则。客户端发现未验证时只提示刷新或新开聊天。' } : {}),
441
459
  refresh: 'Open a new Agent conversation if the skill has not refreshed.' });
442
460
  } catch (error) {
461
+ for (const item of launcherBackup) {
462
+ if (item.existed) atomicWrite(item.file, item.text, item.mode);
463
+ else fs.rmSync(item.file, { force: true });
464
+ }
443
465
  for (const item of backup) {
444
466
  if (item.existed) { atomicWrite(path.join(item.directory, 'SKILL.md'), item.text, 0o644); atomicWrite(path.join(item.directory, ownerFile), item.owner); }
445
467
  else if (fs.existsSync(item.directory)) fs.rmSync(item.directory, { recursive: true });
@@ -565,11 +587,187 @@ function update(home, options, action = 'update') {
565
587
  for (const key of Object.keys(env)) if (key.toLowerCase() === 'path') delete env[key];
566
588
  env.PATH = [path.dirname(process.execPath), inheritedPath].filter(Boolean).join(path.delimiter);
567
589
  Object.assign(env, { FX_DATA_DIAGNOSTICS_HOME: home, NPM_CONFIG_USERCONFIG: path.join(npmHome, 'user.npmrc'), NPM_CONFIG_GLOBALCONFIG: path.join(npmHome, 'global.npmrc'), NPM_CONFIG_CACHE: path.join(npmHome, 'cache'), NPM_CONFIG_REGISTRY: registry, NPM_CONFIG_UPDATE_NOTIFIER: 'false' });
568
- const result = spawnSync(process.execPath, [npmExecutable(), 'exec', '--yes', '--registry', registry, '--package', `${state.package}@${version}`, '--', commandName, 'update', '--activate-package', '--home', home, '--registry', registry], { cwd: npmHome, env, stdio: 'inherit' });
590
+ const background = options['background-job'];
591
+ if (background) assertBackgroundJob(home, background, version);
592
+ const result = spawnSync(process.execPath, [npmExecutable(), 'exec', '--yes', '--registry', registry,
593
+ ...(background ? ['--fetch-retries=0', '--fetch-timeout=15000', '--ignore-scripts'] : []),
594
+ '--package', `${state.package}@${version}`, '--', commandName, 'update', '--activate-package', '--home', home, '--registry', registry,
595
+ ...(background ? ['--background-job', background] : [])], { cwd: npmHome, env, stdio: 'inherit' });
569
596
  if (result.error) throw result.error;
570
597
  process.exitCode = result.status ?? 1;
571
598
  }
572
599
 
600
+ // Automatic updates belong to the shared installation, not to an Agent. Only
601
+ // a new diagnostic entry starts a worker; pinned scripts merely carry notices.
602
+ const autoUpdateFile = 'auto-update.json';
603
+ const autoUpdateLock = '.auto-update.lock';
604
+ const autoUpdateNotice = 'auto-update-notice.json';
605
+ const autoUpdateTimeout = 60_000;
606
+ const autoUpdateEnabled = () => process.env.FX_DATA_DIAGNOSTICS_AUTO_UPDATE !== '0';
607
+ const readOptional = file => { try { return json(file); } catch { return null; } };
608
+
609
+ function deadProcess(pid) {
610
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
611
+ try { process.kill(pid, 0); } catch (error) { return error.code === 'ESRCH'; }
612
+ return false;
613
+ }
614
+
615
+ function newerVersion(candidate, current) {
616
+ if (!versionPattern.test(candidate) || !versionPattern.test(current)) return false;
617
+ const split = value => { const [core, ...pre] = value.split('-'); return { core: core.split('.').map(BigInt), pre: pre.join('-') }; };
618
+ const next = split(candidate), old = split(current);
619
+ for (let i = 0; i < 3; i++) if (next.core[i] !== old.core[i]) return next.core[i] > old.core[i];
620
+ if (!next.pre || !old.pre) return Boolean(old.pre && !next.pre);
621
+ const left = next.pre.split('.'), right = old.pre.split('.');
622
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
623
+ if (left[i] === undefined || right[i] === undefined) return right[i] === undefined;
624
+ if (left[i] === right[i]) continue;
625
+ const numericLeft = /^\d+$/.test(left[i]), numericRight = /^\d+$/.test(right[i]);
626
+ if (numericLeft && numericRight) return BigInt(left[i]) > BigInt(right[i]);
627
+ return numericLeft !== numericRight ? !numericLeft : left[i] > right[i];
628
+ }
629
+ return false;
630
+ }
631
+
632
+ function updateJobOwned(home, id) {
633
+ const lock = readOptional(path.join(home, autoUpdateLock));
634
+ return lock?.id === id && lock.hostname === os.hostname();
635
+ }
636
+
637
+ function releaseUpdateJob(home, id) {
638
+ try {
639
+ if (updateJobOwned(home, id)) fs.rmSync(path.join(home, autoUpdateLock), { force: true });
640
+ } catch { /* Optional background bookkeeping cannot fail a diagnostic call. */ }
641
+ }
642
+
643
+ function saveUpdateJob(home, job, status) {
644
+ if (!updateJobOwned(home, job.id) || ownedState(home, false)?.state?.install_id !== job.install_id) return;
645
+ job.status = status;
646
+ atomicWrite(path.join(home, autoUpdateFile), JSON.stringify(job));
647
+ const message = status === 'updating'
648
+ ? `发现 FX 数据诊断新版 ${job.target_version},正在后台更新。本次分析继续使用已固定版本,更新成功后下次诊断生效。`
649
+ : status === 'updated'
650
+ ? `FX 数据诊断已后台更新至 ${job.target_version},供下次新诊断使用;进行中的分析仍使用各自已固定的版本。`
651
+ : status === 'failed'
652
+ ? 'FX 数据诊断后台检查或更新未完成,继续使用当前可用版本,不影响本次排查。'
653
+ : null;
654
+ if (message) atomicWrite(path.join(home, autoUpdateNotice), JSON.stringify({ id: `${job.id}:${status}`, install_id: job.install_id, status, message }));
655
+ }
656
+
657
+ function autoUpdateDetails(home) {
658
+ if (!autoUpdateEnabled()) return null;
659
+ try {
660
+ const state = ownedState(home, false)?.state;
661
+ if (!state) return null;
662
+ const job = readOptional(path.join(home, autoUpdateFile));
663
+ const details = job?.install_id === state.install_id
664
+ ? { status: job.status, ...(job.target_version ? { target_version: job.target_version } : {}) } : null;
665
+ // Claim one notice without deleting a newer notice written concurrently.
666
+ // Other clients may receive the next state; the worker never writes to a
667
+ // parent's terminal and diagnostic/script stdout keeps its original shape.
668
+ const claimed = path.join(home, `${autoUpdateNotice}.${randomUUID()}.read`);
669
+ try {
670
+ fs.renameSync(path.join(home, autoUpdateNotice), claimed);
671
+ const notice = readOptional(claimed);
672
+ if (notice?.install_id === state.install_id) return { ...details, notification: { id: notice.id, status: notice.status, message: notice.message } };
673
+ } catch { /* No notice, or another CLI already delivered it. */ }
674
+ finally { fs.rmSync(claimed, { force: true }); }
675
+ return details;
676
+ } catch { return null; } // Update bookkeeping must never block diagnosis.
677
+ }
678
+
679
+ function assertBackgroundJob(home, id, version) {
680
+ const job = readOptional(path.join(home, autoUpdateFile));
681
+ const state = ownedState(home, false)?.state;
682
+ if (!updateJobOwned(home, id) || job?.id !== id || job.status !== 'updating'
683
+ || Date.now() >= job.expires_at || deadProcess(job.pid)
684
+ || !state || state.install_id !== job.install_id || state.version !== job.base_version
685
+ || version !== job.target_version) throw new Error('Background update is no longer applicable; keeping the current installation');
686
+ }
687
+
688
+ function startAutoUpdate(home) {
689
+ if (!autoUpdateEnabled()) return;
690
+ let job;
691
+ try {
692
+ const { state } = ownedState(home);
693
+ if (!state) return;
694
+ const lockPath = path.join(home, autoUpdateLock);
695
+ const existing = readOptional(lockPath);
696
+ if (fs.existsSync(lockPath)) {
697
+ // Age alone does not permit stealing a live worker's lock. Failed jobs
698
+ // have no cooldown; after a dead worker the next diagnosis can try again.
699
+ if (existing?.hostname !== os.hostname() || !deadProcess(existing.pid)) return;
700
+ if (readOptional(lockPath)?.id !== existing.id) return;
701
+ fs.unlinkSync(lockPath);
702
+ }
703
+ job = { id: randomUUID(), install_id: state.install_id, base_version: state.version,
704
+ pid: process.pid, hostname: os.hostname(), status: 'checking', expires_at: Date.now() + autoUpdateTimeout };
705
+ const fd = fs.openSync(lockPath, 'wx', 0o600);
706
+ try { fs.writeFileSync(fd, JSON.stringify(job)); } finally { fs.closeSync(fd); }
707
+ saveUpdateJob(home, job, 'checking');
708
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(NPM_TOKEN|NODE_AUTH_TOKEN)$/i.test(key)));
709
+ const child = spawn(process.execPath, [path.join(home, 'releases', state.version, 'bin/core-ai.mjs'), '__background-update', job.id, '--home', home],
710
+ { cwd: home, env, detached: true, stdio: 'ignore', windowsHide: true });
711
+ child.on('error', () => {
712
+ try { saveUpdateJob(home, job, 'failed'); } catch { /* Diagnosis still works. */ }
713
+ finally { releaseUpdateJob(home, job.id); }
714
+ });
715
+ if (child.pid) {
716
+ // Hand the live lock to the worker before the launching CLI exits.
717
+ job.pid = child.pid;
718
+ atomicWrite(lockPath, JSON.stringify(job));
719
+ }
720
+ child.unref();
721
+ } catch (error) {
722
+ if (job && updateJobOwned(home, job.id)) {
723
+ try { saveUpdateJob(home, job, 'failed'); } catch { /* Best effort only. */ }
724
+ finally { releaseUpdateJob(home, job.id); }
725
+ }
726
+ }
727
+ }
728
+
729
+ async function backgroundUpdate(home, id) {
730
+ const job = readOptional(path.join(home, autoUpdateFile));
731
+ if (job?.id !== id || !updateJobOwned(home, id)) return;
732
+ job.pid = process.pid;
733
+ try {
734
+ const state = ownedState(home).state;
735
+ if (!state || state.install_id !== job.install_id || state.version !== job.base_version) return;
736
+ saveUpdateJob(home, job, 'checking');
737
+ const registry = registryOrigin(state.registry);
738
+ const response = await fetch(`${registry}/${encodeURIComponent(state.package)}/latest`,
739
+ { signal: AbortSignal.timeout(3_000), redirect: 'error', headers: { Accept: 'application/json', 'Cache-Control': 'no-cache' } });
740
+ if (!response.ok) throw new Error('Version check failed');
741
+ const latest = await response.json();
742
+ if (latest.name !== state.package || typeof latest.version !== 'string' || !versionPattern.test(latest.version)) throw new Error('Invalid latest version');
743
+ if (!newerVersion(latest.version, state.version)) { saveUpdateJob(home, job, 'current'); return; }
744
+ job.target_version = latest.version;
745
+ saveUpdateJob(home, job, 'updating');
746
+ assertBackgroundJob(home, id, latest.version);
747
+ // Reuse the explicit-version updater in an isolated child. Its output must
748
+ // not keep the initiating Agent tool open or mix with diagnostic results.
749
+ await new Promise((resolve, reject) => {
750
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'update', '--version', latest.version,
751
+ '--home', home, '--background-job', id], { cwd: home, env: process.env, stdio: 'ignore', windowsHide: true });
752
+ const timer = setTimeout(() => {
753
+ // Invalidate activation before stopping the child: npm may have its own
754
+ // subprocess, which must not activate a release after this deadline.
755
+ try { saveUpdateJob(home, job, 'failed'); } catch { /* Best effort. */ }
756
+ child.kill('SIGKILL');
757
+ reject(new Error('Background update timed out'));
758
+ }, Math.max(1, job.expires_at - Date.now()));
759
+ child.once('error', error => { clearTimeout(timer); reject(error); });
760
+ child.once('exit', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error('Background update failed')); });
761
+ });
762
+ if (ownedState(home, false)?.state?.version !== latest.version) throw new Error('Version activation was not confirmed');
763
+ saveUpdateJob(home, job, 'updated');
764
+ } catch {
765
+ try { if (job.status !== 'failed') saveUpdateJob(home, job, 'failed'); } catch { /* Never disturb the active release. */ }
766
+ } finally {
767
+ releaseUpdateJob(home, id);
768
+ }
769
+ }
770
+
573
771
  function python() {
574
772
  const configured = process.env.FX_DATA_DIAGNOSTICS_PYTHON;
575
773
  const candidates = configured ? [[configured]] : process.platform === 'win32' ? [['py', '-3'], ['python'], ['python3']] : [['python3'], ['python']];
@@ -577,7 +775,7 @@ function python() {
577
775
  const probe = spawnSync(args[0], [...args.slice(1), '-I', '-B', '-c', 'import sys; print(sys.executable); sys.exit(0 if sys.version_info >= (3,9) else 1)'], { encoding: 'utf8' });
578
776
  if (probe.status === 0) return { executable: probe.stdout.trim(), available: true };
579
777
  }
580
- return { available: false, requirement: 'Python 3.9+ is required for offline Metric parsing.' };
778
+ return { available: false, requirement: 'Python 3.9+ is required to run bundled scripts.' };
581
779
  }
582
780
 
583
781
  function uninstall(home) {
@@ -595,7 +793,7 @@ function uninstall(home) {
595
793
  // Remove only owned program files. Configuration, npm cache and evidence are retained.
596
794
  // Keep the stable command and state until version cleanup completes, so a
597
795
  // client-denied recursive deletion can be confirmed and retried normally.
598
- for (const name of ['releases', 'bin', 'state.json']) fs.rmSync(path.join(home, name), { recursive: true, force: true });
796
+ for (const name of [autoUpdateLock, autoUpdateFile, autoUpdateNotice, 'releases', 'bin', 'state.json']) fs.rmSync(path.join(home, name), { recursive: true, force: true });
599
797
  const finished = performance.now();
600
798
  const elapsed = (from, to) => Math.round((to - from) * 100) / 100;
601
799
  emit({ ok: true, action: 'uninstall', complete: npmPackage.status !== 'unconfirmed', home,
@@ -625,7 +823,7 @@ function doctor(home) {
625
823
  checks.python = { status: runtime.available ? 'ok' : 'unavailable', ...runtime };
626
824
  const errors = [integrityError, ...(checks.skills.entries || []).filter(entry => entry.status === 'error').map(entry => entry.error)].filter(Boolean);
627
825
  const result = { ok: errors.length === 0, version: installed?.state?.version ?? null, integrity: checks.integrity.status,
628
- python: runtime, capabilities: { metric: checks.integrity.status === 'ok' && runtime.available }, skills: installed?.state?.skills || [], checks };
826
+ python: runtime, skills: installed?.state?.skills || [], checks };
629
827
  if (result.ok) emit(result);
630
828
  else { process.stderr.write(`${JSON.stringify({ ...result, error: errors.join('; ') })}\n`); process.exitCode = 1; }
631
829
  }
@@ -637,11 +835,13 @@ async function main() {
637
835
  if (help) return emit(help);
638
836
  const { command, options, rest } = argumentsFor(process.argv.slice(2));
639
837
  const home = normalizedPath(options.home || process.env.FX_DATA_DIAGNOSTICS_HOME || installedBundleHome(packageRoot) || defaultHome());
838
+ if (command === '__background-update') return backgroundUpdate(home, rest[0]);
839
+ if (options['background-job'] && command !== 'update') throw new Error('--background-job is reserved for background updates');
640
840
  let current;
641
841
  try { current = ownedState(home, false); }
642
842
  catch (error) { if (command === 'doctor') return doctor(home); throw error; }
643
843
  const activeRoot = current?.state ? path.join(home, 'releases', current.state.version) : null;
644
- const pinned = isInstalledBundle(packageRoot, home) && ['context', 'python', 'metric'].includes(command);
844
+ const pinned = isInstalledBundle(packageRoot, home) && ['context', 'python'].includes(command);
645
845
  const migrationRequired = Boolean(current?.state && current.state.lifecycle_protocol !== lifecycleProtocol);
646
846
  if (command === 'doctor') {
647
847
  if (!activeRoot || packageRoot === activeRoot || migrationRequired) return doctor(home);
@@ -649,7 +849,7 @@ async function main() {
649
849
  // diagnostic content is damaged. Fall back here only if the CLI is unsafe.
650
850
  try { bundleMetadata(activeRoot); } catch { return doctor(home); }
651
851
  }
652
- if (migrationRequired && (command === 'install' || (!pinned && ['context', 'python', 'metric', 'version', '--version'].includes(command)))) {
852
+ if (migrationRequired && (command === 'install' || (!pinned && ['context', 'python', 'version', '--version'].includes(command)))) {
653
853
  throw new Error('Existing installation requires migration: run npm install -g @fx-data/ai-diagnostics, then use the new global fx-data-diagnostics update command');
654
854
  }
655
855
  // npm installation is a bootstrap. Re-running it must not switch an existing
@@ -674,17 +874,33 @@ async function main() {
674
874
  if (command === 'update') return update(home, options);
675
875
  if (command === 'rollback') return update(home, options, 'rollback');
676
876
  if (command === 'uninstall') return uninstall(home);
677
- const { pkg, manifest, content } = verifyBundle(packageRoot, { allowMacMetadata: isInstalledBundle(packageRoot, home) });
877
+ const { pkg, manifest } = verifyBundle(packageRoot, { allowMacMetadata: isInstalledBundle(packageRoot, home) });
678
878
  if (command === '--version' || command === 'version') return emit({ version: pkg.version, package: pkg.name });
679
- if (command === 'status') return emit({ ok: true, installed: Boolean(current?.state), home, ...current?.state, lifecycle_protocol: current?.state?.lifecycle_protocol ?? null, migration_required: migrationRequired, launcher: current?.state ? launchers(home).executable : null, launcher_script: current?.state ? launchers(home).script : null, node: process.execPath });
680
- if (command === 'context') return emit(contextFor(packageRoot, home, pkg, manifest));
681
- if (command === 'session') return emit({ directory: fs.mkdtempSync(path.join(os.tmpdir(), 'core-ai-')) });
682
- if (command === 'metric' || command === 'python') {
879
+ if (command === 'status') {
880
+ const auto = autoUpdateDetails(home);
881
+ return emit({ ok: true, installed: Boolean(current?.state), home, ...current?.state, lifecycle_protocol: current?.state?.lifecycle_protocol ?? null, migration_required: migrationRequired, launcher: current?.state ? launchers(home).executable : null, launcher_script: current?.state ? launchers(home).script : null, node: process.execPath, ...(auto ? { auto_update: auto } : {}) });
882
+ }
883
+ if (command === 'context') {
884
+ // Capture paths before starting the worker. Direct calls to a pinned release
885
+ // are continuations, even if that release is still the current one.
886
+ const context = contextFor(packageRoot, home, pkg, manifest);
887
+ const auto = autoUpdateDetails(home);
888
+ const directPinned = isInstalledBundle(packageRoot, home)
889
+ && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
890
+ if (current?.state && !directPinned) startAutoUpdate(home);
891
+ return emit({ ...context, ...(auto ? { auto_update: auto } : {}) });
892
+ }
893
+ if (command === 'session') {
894
+ const auto = autoUpdateDetails(home);
895
+ return emit({ directory: fs.mkdtempSync(path.join(os.tmpdir(), 'core-ai-')), ...(auto ? { auto_update: auto } : {}) });
896
+ }
897
+ if (command === 'python') {
683
898
  const runtime = python();
684
899
  if (!runtime.available) throw new Error(runtime.requirement);
685
- const args = command === 'metric' ? [path.join(content, '.agents/skills/analyze-engine-metric/scripts/analyze_engine_metric.py'), ...rest] : rest;
686
- if (!args.length) throw new Error('A script or Python option is required');
687
- const result = spawnSync(runtime.executable, ['-I', '-B', ...args], { stdio: 'inherit' });
900
+ if (!rest.length) throw new Error('A script or Python option is required');
901
+ const auto = autoUpdateDetails(home);
902
+ if (auto?.notification) process.stderr.write(`${JSON.stringify({ fx_data_diagnostics: { auto_update: auto } })}\n`);
903
+ const result = spawnSync(runtime.executable, ['-I', '-B', ...rest], { stdio: 'inherit' });
688
904
  if (result.error) throw result.error;
689
905
  process.exitCode = result.status ?? 1;
690
906
  return;