@adhdev/daemon-core 0.9.82-rc.31 → 0.9.82-rc.311

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 (294) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +23 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  13. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  14. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  15. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  16. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  17. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  18. package/dist/commands/cli-manager.d.ts +2 -1
  19. package/dist/commands/handler.d.ts +110 -0
  20. package/dist/commands/mesh-coordinator.d.ts +85 -1
  21. package/dist/commands/router.d.ts +214 -1
  22. package/dist/config/chat-history.d.ts +9 -0
  23. package/dist/config/config.d.ts +5 -0
  24. package/dist/config/mesh-config.d.ts +72 -1
  25. package/dist/git/git-commands.d.ts +9 -1
  26. package/dist/git/git-diff.d.ts +6 -0
  27. package/dist/git/git-executor.d.ts +11 -0
  28. package/dist/git/git-status.d.ts +9 -0
  29. package/dist/git/git-types.d.ts +2 -50
  30. package/dist/index.d.ts +51 -12
  31. package/dist/index.js +34705 -11938
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +38412 -15757
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/installer.d.ts +1 -4
  36. package/dist/ipc/local-ipc-server.d.ts +91 -0
  37. package/dist/launch.d.ts +1 -1
  38. package/dist/logging/async-batch-writer.d.ts +10 -0
  39. package/dist/mesh/contracts.d.ts +164 -0
  40. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  41. package/dist/mesh/coordinator-registry.d.ts +59 -0
  42. package/dist/mesh/mesh-active-work.d.ts +108 -0
  43. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  44. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  45. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  46. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  47. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  48. package/dist/mesh/mesh-events.d.ts +6 -49
  49. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  50. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  51. package/dist/mesh/mesh-init.d.ts +86 -0
  52. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  53. package/dist/mesh/mesh-ledger.d.ts +77 -1
  54. package/dist/mesh/mesh-missions.d.ts +116 -0
  55. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  56. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  57. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  58. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  59. package/dist/mesh/mesh-routing.d.ts +70 -0
  60. package/dist/mesh/mesh-runtime-store.d.ts +395 -0
  61. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  62. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  63. package/dist/mesh/mesh-work-queue.d.ts +204 -5
  64. package/dist/mesh/preview-freshness.d.ts +18 -0
  65. package/dist/mesh/refine-config.d.ts +215 -0
  66. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  67. package/dist/providers/acp-provider-instance.d.ts +2 -0
  68. package/dist/providers/approval-utils.d.ts +13 -0
  69. package/dist/providers/cli-provider-instance.d.ts +61 -3
  70. package/dist/providers/contracts.d.ts +130 -6
  71. package/dist/providers/external-sources.d.ts +71 -0
  72. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  73. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  74. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  75. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  76. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  77. package/dist/providers/native-history/index.d.ts +13 -0
  78. package/dist/providers/provider-instance-manager.d.ts +13 -0
  79. package/dist/providers/provider-instance.d.ts +13 -1
  80. package/dist/providers/provider-loader.d.ts +26 -4
  81. package/dist/providers/provider-trust.d.ts +31 -0
  82. package/dist/providers/read-chat-contract.d.ts +29 -0
  83. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  84. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  85. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  86. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  87. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  88. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  89. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  90. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  91. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  92. package/dist/providers/sdk/v1/index.d.ts +30 -0
  93. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  94. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  95. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  96. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  97. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  98. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  99. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  100. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  101. package/dist/providers/spec/adapter.d.ts +59 -0
  102. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  103. package/dist/providers/spec/evaluator.d.ts +23 -0
  104. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  105. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  106. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  107. package/dist/providers/spec/fsm-types.d.ts +128 -0
  108. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  109. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  110. package/dist/providers/spec/route.d.ts +4 -0
  111. package/dist/providers/spec/types.d.ts +180 -0
  112. package/dist/providers/transcript-v2.d.ts +176 -0
  113. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  114. package/dist/repo-mesh-types.d.ts +291 -13
  115. package/dist/sessions/registry.d.ts +6 -0
  116. package/dist/shared-types-extra.d.ts +2 -4
  117. package/dist/shared-types.d.ts +42 -1
  118. package/dist/status/normalize.d.ts +1 -1
  119. package/dist/status/normalize.js +1 -0
  120. package/dist/status/normalize.js.map +1 -1
  121. package/dist/status/normalize.mjs +1 -0
  122. package/dist/status/normalize.mjs.map +1 -1
  123. package/dist/status/reporter.d.ts +2 -0
  124. package/dist/status/snapshot.d.ts +1 -0
  125. package/dist/types.d.ts +5 -0
  126. package/package.json +7 -3
  127. package/src/agent-stream/poller.ts +2 -3
  128. package/src/boot/daemon-lifecycle.ts +55 -12
  129. package/src/boot/process-hardening.ts +89 -0
  130. package/src/build-info.ts +73 -0
  131. package/src/chat/source-machine.ts +534 -0
  132. package/src/chat/source-resolver.ts +0 -0
  133. package/src/chat/subscription-updates.ts +20 -1
  134. package/src/cli-adapter-types.d.ts +1 -0
  135. package/src/cli-adapter-types.ts +22 -2
  136. package/src/cli-adapters/cli-script-runner.ts +421 -0
  137. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  138. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  139. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  140. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  141. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  142. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  143. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  144. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  145. package/src/cli-adapters/pty-transport.ts +2 -1
  146. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  147. package/src/cli-adapters/resolve-executable.ts +45 -0
  148. package/src/cli-adapters/session-host-transport.ts +2 -1
  149. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  150. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  151. package/src/cli-adapters/terminal-screen.ts +16 -81
  152. package/src/commands/chat-commands.ts +1822 -61
  153. package/src/commands/cli-manager.ts +286 -14
  154. package/src/commands/handler.ts +809 -2
  155. package/src/commands/mesh-coordinator.ts +332 -122
  156. package/src/commands/router.ts +6041 -589
  157. package/src/commands/stream-commands.ts +8 -0
  158. package/src/commands/upgrade-helper.ts +29 -0
  159. package/src/config/chat-history.ts +483 -24
  160. package/src/config/config.ts +12 -0
  161. package/src/config/mesh-config.ts +320 -4
  162. package/src/config/recent-activity.ts +8 -2
  163. package/src/daemon/dev-auto-implement.ts +3 -2
  164. package/src/daemon/dev-cli-debug.ts +10 -1
  165. package/src/detection/ide-detector.ts +26 -16
  166. package/src/git/git-commands.ts +42 -12
  167. package/src/git/git-diff.ts +53 -0
  168. package/src/git/git-executor.ts +12 -0
  169. package/src/git/git-status.ts +302 -18
  170. package/src/git/git-types.ts +14 -62
  171. package/src/git/git-worktree.ts +8 -1
  172. package/src/index.ts +142 -11
  173. package/src/installer.d.ts +1 -1
  174. package/src/installer.ts +8 -6
  175. package/src/ipc/local-ipc-server.ts +278 -0
  176. package/src/launch.d.ts +1 -1
  177. package/src/launch.ts +37 -28
  178. package/src/logging/async-batch-writer.ts +55 -0
  179. package/src/logging/logger.ts +2 -1
  180. package/src/mesh/contracts.ts +329 -0
  181. package/src/mesh/coordinator-prompt.ts +234 -31
  182. package/src/mesh/coordinator-registry.ts +121 -0
  183. package/src/mesh/mesh-active-work.ts +469 -0
  184. package/src/mesh/mesh-delivery-policy.ts +315 -0
  185. package/src/mesh/mesh-events-coordinator.ts +1942 -0
  186. package/src/mesh/mesh-events-pending.ts +508 -0
  187. package/src/mesh/mesh-events-stale.ts +290 -0
  188. package/src/mesh/mesh-events-utils.ts +254 -0
  189. package/src/mesh/mesh-events.ts +30 -1035
  190. package/src/mesh/mesh-fast-forward.ts +839 -0
  191. package/src/mesh/mesh-host-ownership.ts +73 -0
  192. package/src/mesh/mesh-init.ts +260 -0
  193. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  194. package/src/mesh/mesh-ledger.ts +589 -102
  195. package/src/mesh/mesh-missions.ts +248 -0
  196. package/src/mesh/mesh-reconcile-loop.ts +466 -0
  197. package/src/mesh/mesh-refine-batch.ts +197 -0
  198. package/src/mesh/mesh-refine-status.ts +231 -0
  199. package/src/mesh/mesh-review-inbox.ts +307 -0
  200. package/src/mesh/mesh-routing.ts +291 -0
  201. package/src/mesh/mesh-runtime-store.ts +1565 -0
  202. package/src/mesh/mesh-task-stats.ts +154 -0
  203. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  204. package/src/mesh/mesh-work-queue.ts +703 -140
  205. package/src/mesh/preview-freshness.ts +118 -0
  206. package/src/mesh/refine-config.ts +403 -0
  207. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  208. package/src/providers/acp-provider-instance.ts +15 -1
  209. package/src/providers/approval-utils.d.ts +4 -0
  210. package/src/providers/approval-utils.ts +47 -5
  211. package/src/providers/chat-message-normalization.ts +4 -11
  212. package/src/providers/cli-provider-instance.ts +953 -87
  213. package/src/providers/contracts.d.ts +55 -0
  214. package/src/providers/contracts.ts +141 -6
  215. package/src/providers/extension-provider-instance.ts +5 -1
  216. package/src/providers/external-sources.ts +218 -0
  217. package/src/providers/ide-provider-instance.ts +25 -6
  218. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  219. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  220. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  221. package/src/providers/native-history/dispatcher.ts +340 -0
  222. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  223. package/src/providers/native-history/index.ts +30 -0
  224. package/src/providers/provider-instance-manager.ts +30 -0
  225. package/src/providers/provider-instance.ts +10 -1
  226. package/src/providers/provider-loader.ts +584 -50
  227. package/src/providers/provider-schema.ts +87 -14
  228. package/src/providers/provider-trust.ts +114 -0
  229. package/src/providers/read-chat-contract.ts +76 -16
  230. package/src/providers/sdk/README.md +49 -0
  231. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  232. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  233. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  234. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  235. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  236. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  237. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  238. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  239. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  240. package/src/providers/sdk/v1/index.ts +152 -0
  241. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  242. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  243. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  244. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  245. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  246. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  247. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  248. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  249. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  250. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  251. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  252. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  253. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  254. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  255. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  256. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  257. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  258. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  259. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  260. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  261. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  262. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  263. package/src/providers/sdk/v1/validators/index.ts +19 -0
  264. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  265. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  266. package/src/providers/spec/adapter.ts +155 -0
  267. package/src/providers/spec/cli-adapter.ts +980 -0
  268. package/src/providers/spec/evaluator.ts +345 -0
  269. package/src/providers/spec/fsm-driver.ts +888 -0
  270. package/src/providers/spec/fsm-evaluator.ts +255 -0
  271. package/src/providers/spec/fsm-loader.ts +112 -0
  272. package/src/providers/spec/fsm-types.ts +222 -0
  273. package/src/providers/spec/native-history-executor.ts +1035 -0
  274. package/src/providers/spec/pre-launch-trust.ts +104 -0
  275. package/src/providers/spec/route.ts +51 -0
  276. package/src/providers/spec/types.ts +226 -0
  277. package/src/providers/transcript-v2.ts +567 -0
  278. package/src/providers/types/interactive-prompt.ts +536 -0
  279. package/src/providers/version-archive.ts +38 -20
  280. package/src/repo-mesh-types.ts +376 -13
  281. package/src/sessions/registry.ts +6 -0
  282. package/src/shared-types-extra.ts +2 -4
  283. package/src/shared-types.ts +45 -1
  284. package/src/status/builders.ts +26 -6
  285. package/src/status/normalize.ts +2 -0
  286. package/src/status/reporter.ts +15 -0
  287. package/src/status/snapshot.ts +84 -25
  288. package/src/system/host-memory.ts +29 -12
  289. package/src/types.ts +5 -0
  290. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  291. package/dist/mesh/mesh-sync.d.ts +0 -53
  292. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  293. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  294. package/src/mesh/mesh-sync.ts +0 -111
@@ -30,8 +30,34 @@ import type {
30
30
  ResolvedProvider,
31
31
  } from './contracts.js';
32
32
  import { validateProviderDefinition } from './provider-schema.js';
33
+ import {
34
+ loadProvidersActive,
35
+ resolveActiveSource,
36
+ } from './external-sources.js';
33
37
  import type { ProviderSourceMode } from '../config/config.js';
34
38
  import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
39
+ import { executeNativeHistory } from './spec/native-history-executor.js';
40
+ import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
41
+
42
+ /**
43
+ * Adds a provider-script root to the require whitelist. Wrapped in a
44
+ * try/catch + null check so a loader hot-path can't crash on a path
45
+ * that doesn't exist yet or one the whitelist hook rejects.
46
+ *
47
+ * The require-whitelist module is loaded lazily on first call. Eagerly
48
+ * top-level importing it pulls `node:fs.realpathSync.native` into
49
+ * module evaluation, which breaks unit tests that partially mock `fs`
50
+ * (e.g. test/commands/get-logs-incremental.test.ts mocks only
51
+ * existsSync + readFileSync). Lazy load keeps that mock surface valid.
52
+ */
53
+ function registerProviderScriptRootSafely(root: string | null | undefined): void {
54
+ if (!root || typeof root !== 'string') return;
55
+ try {
56
+ const { registerProviderScriptRoot } =
57
+ require('./sdk/v1/sandbox/require-whitelist.js') as typeof import('./sdk/v1/sandbox/require-whitelist.js');
58
+ registerProviderScriptRoot(root);
59
+ } catch { /* boot-time only — swallow */ }
60
+ }
35
61
 
36
62
  interface ProviderAvailabilityState {
37
63
  installed: boolean;
@@ -92,7 +118,9 @@ export class ProviderLoader {
92
118
  }
93
119
 
94
120
  private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
121
+ private static readonly REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
95
122
  private static readonly META_FILE = '.meta.json';
123
+ private static readonly REGISTRY_META_FILE = '.registry-meta.json';
96
124
  private static readonly REPO_PROVIDER_DIRNAME = 'adhdev-providers';
97
125
  private static readonly SIBLING_MARKER_FILE = '.adhdev-provider-root';
98
126
  private static readonly SIBLING_ENV_VAR = 'ADHDEV_USE_SIBLING_PROVIDERS';
@@ -193,6 +221,33 @@ export class ProviderLoader {
193
221
  sourceMode: options?.sourceMode,
194
222
  disableUpstream: options?.disableUpstream,
195
223
  });
224
+
225
+ // One-time migration: ~/.adhdev/marketplace → ~/.adhdev/external.
226
+ // The directory was renamed when the "marketplace" install model was
227
+ // dropped in favour of explicit external git sources. Best-effort:
228
+ // if the rename fails we leave both dirs in place and log so the user
229
+ // can investigate.
230
+ this.migrateMarketplaceDirToExternal();
231
+ }
232
+
233
+ private migrateMarketplaceDirToExternal(): void {
234
+ try {
235
+ const home = os.homedir();
236
+ const oldDir = path.join(home, '.adhdev', 'marketplace');
237
+ const newDir = path.join(home, '.adhdev', 'external');
238
+ if (!fs.existsSync(oldDir)) return;
239
+ if (fs.existsSync(newDir)) {
240
+ // Both exist — don't merge. Leave old in place; surface in logs so
241
+ // the user can decide what to keep. Loader still loads from
242
+ // external/ only, so old marketplace/ becomes inert.
243
+ this.log(`Migration skipped: both ~/.adhdev/marketplace and ~/.adhdev/external exist (marketplace dir is now inert and can be removed manually).`);
244
+ return;
245
+ }
246
+ fs.renameSync(oldDir, newDir);
247
+ this.log(`Migrated ~/.adhdev/marketplace → ~/.adhdev/external (one-time rename after provider source-layer cleanup).`);
248
+ } catch (e: any) {
249
+ this.log(`Marketplace→external migration failed: ${e?.message || e}`);
250
+ }
196
251
  }
197
252
 
198
253
  private log(msg: string): void {
@@ -224,7 +279,12 @@ export class ProviderLoader {
224
279
  * Highest-priority editable overrides come first.
225
280
  */
226
281
  getProviderRoots(): string[] {
227
- return [this.userDir, this.upstreamDir];
282
+ // Order matters: user customs > external (3rd-party sources) > upstream
283
+ // (official auto-sync). findProviderDirInternal walks this list in order
284
+ // to locate the provider dir containing the scripts/, so external must
285
+ // be included here even though loadAll() also reads it directly.
286
+ const externalDir = path.join(os.homedir(), '.adhdev', 'external');
287
+ return [this.userDir, externalDir, this.upstreamDir];
228
288
  }
229
289
 
230
290
  getSourceConfig(): ProviderSourceConfigSnapshot {
@@ -316,16 +376,19 @@ export class ProviderLoader {
316
376
 
317
377
  /**
318
378
  * Load all providers (3-tier priority)
319
- * 1. .upstream/ (GitHub auto-download — primary source)
320
- * 2. User custom (~/.adhdev/providers/ excluding .upstream)
321
- * User custom always wins (highest priority).
379
+ * 1. ~/.adhdev/providers/.upstream/ official git, auto-synced
380
+ * 2. ~/.adhdev/external/ 3rd-party git sources, user-added,
381
+ * bundled providers may include arbitrary JS (untrusted by default)
382
+ * 3. ~/.adhdev/providers/ (excluding .upstream) — user-authored customs,
383
+ * always wins
384
+ * Highest priority listed last (overwrites earlier loads).
322
385
  * If .upstream/ is empty, call fetchLatest() before loadAll().
323
386
  */
324
387
  loadAll(): void {
325
388
  this.providers.clear();
326
389
  this.providerAvailability.clear();
327
390
 
328
- // 1. Load upstream (GitHub auto-download — primary source)
391
+ // 1. Load upstream (GitHub auto-download — primary official source)
329
392
  let upstreamCount = 0;
330
393
  if (!this.disableUpstream && fs.existsSync(this.upstreamDir)) {
331
394
  upstreamCount = this.loadDir(this.upstreamDir);
@@ -336,7 +399,90 @@ export class ProviderLoader {
336
399
  this.log('Upstream loading disabled (sourceMode=no-upstream)');
337
400
  }
338
401
 
339
- // 2. Load user custom (excluding .upstream — highest priority, never auto-updated)
402
+ // 2. Load external providers from ~/.adhdev/external/<source-name>/
403
+ // (3rd-party git sources). Overrides upstream but is itself overridden
404
+ // by user customs in step 3.
405
+ //
406
+ // Each registered source is a separate subdirectory so two sources can
407
+ // both expose the same provider type without overwriting each other.
408
+ // When more than one source provides the same type, providers-active.json
409
+ // chooses the active one; without an explicit choice we deterministically
410
+ // pick the first in disk-walk order and log the ambiguity so the user
411
+ // can resolve it from the dashboard.
412
+ //
413
+ // Any non-spec manifest (tui block / overrides / scriptDir) coming from
414
+ // an external source runs JavaScript the daemon hasn't audited, so
415
+ // dashboards must surface an "untrusted source" badge before letting
416
+ // the user enable them.
417
+ const externalDir = path.join(os.homedir(), '.adhdev', 'external');
418
+ if (fs.existsSync(externalDir)) {
419
+ // Legacy layout (pre-source-namespace): manifests sit directly at
420
+ // external/<category>/<type>/. Detect by presence of category dirs at
421
+ // the root and migrate inline by treating the whole tree as a single
422
+ // implicit source. Loader behavior unchanged for legacy callers.
423
+ const rootEntries = (() => {
424
+ try { return fs.readdirSync(externalDir, { withFileTypes: true }); }
425
+ catch { return [] as fs.Dirent[]; }
426
+ })();
427
+ const KNOWN_CATEGORIES = new Set(['cli', 'ide', 'extension', 'acp']);
428
+ const looksLegacy = rootEntries.some(e => e.isDirectory() && KNOWN_CATEGORIES.has(e.name));
429
+ if (looksLegacy) {
430
+ // Tree shape predates per-source dirs — treat the whole thing as a
431
+ // single anonymous source so existing installs keep working until
432
+ // they're migrated to a real source registration.
433
+ const externalCount = this.loadDir(externalDir);
434
+ if (externalCount > 0) {
435
+ this.log(`Loaded ${externalCount} external providers (legacy unnamed source)`);
436
+ }
437
+ } else {
438
+ // New layout: external/<source-name>/<category>/<type>/…
439
+ const activeFile = loadProvidersActive();
440
+ let totalLoaded = 0;
441
+ const ambiguousTypes: { type: string; chosen: string; candidates: string[] }[] = [];
442
+ // Per-source load, then filter by active-selection: for each type
443
+ // present in more than one source, only the active source's copy
444
+ // is left in this.providers.
445
+ for (const sourceEntry of rootEntries) {
446
+ if (!sourceEntry.isDirectory()) continue;
447
+ const sourceDir = path.join(externalDir, sourceEntry.name);
448
+ const sourceLoaded = this.loadDir(sourceDir);
449
+ if (sourceLoaded > 0) {
450
+ totalLoaded += sourceLoaded;
451
+ this.log(`Loaded ${sourceLoaded} providers from external source "${sourceEntry.name}"`);
452
+ }
453
+ }
454
+ // Resolve ambiguities — when the same type came from multiple
455
+ // sources, the last load wins by default. Replay with the active
456
+ // selection so the user-chosen source ends up winning.
457
+ for (const [type] of this.providers) {
458
+ const prov = this.providers.get(type);
459
+ if (!prov) continue;
460
+ const resolved = resolveActiveSource(prov.category, type, activeFile);
461
+ if (resolved.candidates.length <= 1) continue;
462
+ if (resolved.ambiguous) {
463
+ ambiguousTypes.push({ type, chosen: resolved.source ?? '?', candidates: resolved.candidates });
464
+ }
465
+ if (resolved.source && resolved.source !== '?') {
466
+ const sourceDir = path.join(externalDir, resolved.source);
467
+ // Reload only this source's copy of the conflicting type so it
468
+ // overwrites whatever else won the initial pass.
469
+ const reloadCount = this.loadDir(sourceDir);
470
+ // reloadCount is a sanity check — we expect ≥1
471
+ if (reloadCount === 0) {
472
+ this.log(`Active source "${resolved.source}" no longer provides ${type}`);
473
+ }
474
+ }
475
+ }
476
+ if (totalLoaded > 0) {
477
+ this.log(`Loaded ${totalLoaded} external providers (3rd-party sources)`);
478
+ }
479
+ for (const a of ambiguousTypes) {
480
+ this.log(`Ambiguous provider "${a.type}" — provided by [${a.candidates.join(', ')}], defaulted to "${a.chosen}". Set the active source from the dashboard to silence this warning.`);
481
+ }
482
+ }
483
+ }
484
+
485
+ // 3. Load user custom (excluding .upstream — highest priority, never auto-updated)
340
486
  if (fs.existsSync(this.userDir)) {
341
487
  const userCount = this.loadDir(this.userDir, ['.upstream']);
342
488
  if (userCount > 0) {
@@ -909,18 +1055,28 @@ export class ProviderLoader {
909
1055
 
910
1056
  for (const entry of compat) {
911
1057
  if (this.matchesVersion(currentVersion, entry.ideVersion)) {
912
- const loaded = this.loadScriptsFromDir(type, entry.scriptDir);
913
- if (loaded) {
914
- resolved.scripts = loaded;
915
- this.debugLog(` [compatibility] ${type} v${currentVersion} ${entry.scriptDir}`);
916
- resolved._resolvedScriptDir = entry.scriptDir;
917
- resolved._resolvedScriptsSource = `compatibility:${entry.ideVersion}`;
918
- if (providerDir) {
919
- const fullDir = path.join(providerDir, entry.scriptDir);
920
- resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
921
- ? path.join(fullDir, 'scripts.js')
922
- : fullDir;
1058
+ // entry.scriptDir is optional now — spec-driven providers (agy,
1059
+ // codex on >=0.137, claude on >=2.1) only ship `spec` here, so
1060
+ // there's nothing to load from the filesystem. SpecCliAdapter
1061
+ // takes over via the `spec` path later in this method.
1062
+ if (entry.scriptDir) {
1063
+ const loaded = this.loadScriptsFromDir(type, entry.scriptDir);
1064
+ if (loaded) {
1065
+ resolved.scripts = loaded;
1066
+ this.debugLog(` [compatibility] ${type} v${currentVersion} → ${entry.scriptDir}`);
1067
+ resolved._resolvedScriptDir = entry.scriptDir;
1068
+ resolved._resolvedScriptsSource = `compatibility:${entry.ideVersion}`;
1069
+ if (providerDir) {
1070
+ const fullDir = path.join(providerDir, entry.scriptDir);
1071
+ resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
1072
+ ? path.join(fullDir, 'scripts.js')
1073
+ : fullDir;
1074
+ }
1075
+ matched = true;
923
1076
  }
1077
+ } else {
1078
+ // Spec-only entry — still counts as a match so the
1079
+ // defaultScriptDir fallback below doesn't kick in.
924
1080
  matched = true;
925
1081
  }
926
1082
  break; // first match wins
@@ -988,7 +1144,12 @@ export class ProviderLoader {
988
1144
  }
989
1145
 
990
1146
  // 3. Composite override (OS + version)
991
- if (base.overrides) {
1147
+ // Legacy shape: base.overrides is an Array<{ when: {os,version}, scripts }>.
1148
+ // v1 manifests (Phase 3-4) repurposed `overrides` as an object map of
1149
+ // capability overrides (e.g. { detectStatus: { path, schema } }), which is
1150
+ // consumed by the SDK builders, not by this resolver. Only iterate when
1151
+ // the field is in the legacy array shape.
1152
+ if (Array.isArray(base.overrides)) {
992
1153
  for (const override of base.overrides) {
993
1154
  const osMatch = !override.when.os || override.when.os === currentOs;
994
1155
  const verMatch = !override.when.version || (currentVersion && this.matchesVersion(currentVersion, override.when.version));
@@ -996,6 +1157,40 @@ export class ProviderLoader {
996
1157
  resolved.scripts = { ...resolved.scripts, ...override.scripts };
997
1158
  }
998
1159
  }
1160
+ } else if (base.overrides && typeof base.overrides === 'object') {
1161
+ // v1 manifest shape: { detectStatus: { path }, parseSession: { path }, ... }
1162
+ // Each script name maps to a path inside the provider directory. We load
1163
+ // the file and merge its export(s) into resolved.scripts. Lets a
1164
+ // provider override a single primitive (e.g. just detectStatus) while
1165
+ // letting the SDK synthesize the rest from the tui block.
1166
+ const providerDir = this.findProviderDirInternal(base.type);
1167
+ if (providerDir) {
1168
+ for (const [scriptName, override] of Object.entries(base.overrides as Record<string, any>)) {
1169
+ if (!override || typeof override.path !== 'string') continue;
1170
+ const fullPath = path.join(providerDir, override.path);
1171
+ if (!fs.existsSync(fullPath)) {
1172
+ this.log(` [overrides] ${base.type}: ${scriptName} path not found: ${fullPath}`);
1173
+ continue;
1174
+ }
1175
+ try {
1176
+ // Override scripts go through the same whitelist gate as the
1177
+ // main scripts dir. Use the provider parent root so a v1
1178
+ // override can still require ../_shared helpers.
1179
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1180
+ delete require.cache[require.resolve(fullPath)];
1181
+ const fn = require(fullPath);
1182
+ const target = typeof fn === 'function' ? fn : (fn && fn[scriptName]);
1183
+ if (typeof target === 'function') {
1184
+ resolved.scripts = { ...resolved.scripts, [scriptName]: target } as any;
1185
+ this.log(` [overrides] ${base.type}: ${scriptName} loaded from ${override.path}`);
1186
+ } else {
1187
+ this.log(` [overrides] ${base.type}: ${scriptName} export missing in ${override.path}`);
1188
+ }
1189
+ } catch (e: any) {
1190
+ this.log(` [overrides] ${base.type}: ${scriptName} require failed: ${e?.message || e}`);
1191
+ }
1192
+ }
1193
+ }
999
1194
  }
1000
1195
 
1001
1196
  if ((resolved.category === 'cli' || resolved.category === 'acp') && resolved.spawn?.command) {
@@ -1006,6 +1201,122 @@ export class ProviderLoader {
1006
1201
  };
1007
1202
  }
1008
1203
 
1204
+ // (spec migration) Late-binding spec.json native-history hook. Runs
1205
+ // *after* every script-loading path (compatibility / defaultScriptDir /
1206
+ // overrides) so it deterministically wins over a legacy v1 scripts.js
1207
+ // export. Three modes, picked by spec.json's native_history block:
1208
+ // 1. source — declarative jsonl/sqlite executor (new-provider path,
1209
+ // no daemon change needed for new on-disk formats)
1210
+ // 2. override_path — provider-supplied reader file (escape hatch for
1211
+ // exotic formats); module default-exports a reader fn
1212
+ // 3. reader — built-in reader id (claude-cli / codex-cli /
1213
+ // antigravity-cli / hermes-cli), kept for backwards
1214
+ // compatibility with the four shipped providers
1215
+ if (providerDir) {
1216
+ try {
1217
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1218
+ const fs = require('node:fs');
1219
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1220
+ const path = require('node:path');
1221
+ // Pick the right spec file for the detected CLI version. Resolution
1222
+ // order:
1223
+ // 1. compatibility[i].spec where ideVersion matches currentVersion
1224
+ // (lets a provider ship specs/2.0.json, specs/2.1.json, etc.
1225
+ // alongside the matching scriptDir)
1226
+ // 2. specs/default.json — explicit fallback
1227
+ // 3. spec.json — legacy single-spec layout
1228
+ // Missing files fall through silently to the next candidate.
1229
+ const candidates: string[] = [];
1230
+ if (Array.isArray((base as any).compatibility)) {
1231
+ for (const entry of (base as any).compatibility) {
1232
+ if (typeof entry?.spec !== 'string') continue;
1233
+ // If currentVersion is unknown (cli-manager hasn't probed yet)
1234
+ // we still let compatibility entries that don't pin a version
1235
+ // through, plus any entry whose pin matches.
1236
+ const matches = !entry.ideVersion
1237
+ || (currentVersion && this.matchesVersion(currentVersion, entry.ideVersion))
1238
+ || !currentVersion;
1239
+ if (matches) candidates.push(path.join(providerDir, entry.spec));
1240
+ }
1241
+ }
1242
+ candidates.push(path.join(providerDir, 'specs', 'default.json'));
1243
+ candidates.push(path.join(providerDir, 'spec.json'));
1244
+ const specPath = candidates.find((p: string) => fs.existsSync(p));
1245
+ if (specPath) {
1246
+ // Hand the resolved spec path off to route.ts via a hidden field
1247
+ // so the routing layer doesn't have to repeat the candidate walk.
1248
+ (resolved as any)._resolvedSpecPath = specPath;
1249
+ // Extract control_bar + native_history directly from the JSON header.
1250
+ let specControls: any[] | undefined;
1251
+ let nh: any | undefined;
1252
+ try {
1253
+ const rawSpec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1254
+ specControls = rawSpec.control_bar;
1255
+ nh = rawSpec.native_history;
1256
+ } catch { /* unreadable spec — leave controls/native unavailable */ }
1257
+ // Stub each control_bar entry as a provider.scripts.<id>. The
1258
+ // upstream invoke_provider_script gate checks that the script
1259
+ // name exists on provider.scripts before calling adapter.invokeScript;
1260
+ // for spec providers the *actual* dispatch happens inside
1261
+ // SpecCliAdapter.invokeScript which maps the name to control_bar.
1262
+ // The stub is just a presence marker so the gate doesn't reject.
1263
+ if (specControls && specControls.length > 0) {
1264
+ resolved.scripts = { ...(resolved.scripts || {}) };
1265
+ for (const ctl of specControls) {
1266
+ if (!(resolved.scripts as any)[ctl.id]) {
1267
+ (resolved.scripts as any)[ctl.id] = (..._args: unknown[]) => ({
1268
+ __spec_control: true,
1269
+ controlId: ctl.id,
1270
+ actionType: ctl.action.type,
1271
+ });
1272
+ }
1273
+ }
1274
+ }
1275
+ if (nh) {
1276
+ let reader: ((input: any) => any) | null = null;
1277
+ let format = 'spec';
1278
+
1279
+ if (nh.source) {
1280
+ format = `spec-${nh.source.kind}`;
1281
+ reader = (input: any) => executeNativeHistory(nh, input);
1282
+ } else if (nh.override_path) {
1283
+ const overrideFile = path.resolve(providerDir, nh.override_path);
1284
+ if (fs.existsSync(overrideFile)) {
1285
+ try {
1286
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1287
+ delete require.cache[require.resolve(overrideFile)];
1288
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1289
+ const mod = require(overrideFile);
1290
+ const fn = typeof mod === 'function' ? mod : (mod && typeof mod.default === 'function' ? mod.default : null);
1291
+ if (fn) {
1292
+ format = 'spec-override';
1293
+ reader = (input: any) => fn(input);
1294
+ }
1295
+ } catch { /* fall through — leave native unavailable */ }
1296
+ }
1297
+ } else if (nh.reader) {
1298
+ const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
1299
+ format = nh.reader;
1300
+ reader = (input: any) => dispatch(input);
1301
+ }
1302
+
1303
+ if (reader) {
1304
+ resolved.scripts = { ...(resolved.scripts || {}) };
1305
+ (resolved.scripts as any).readNativeHistory = reader;
1306
+ (resolved as any).nativeHistory = {
1307
+ format,
1308
+ watchPath: undefined,
1309
+ scripts: { readSession: 'readNativeHistory' },
1310
+ mode: 'native-source',
1311
+ };
1312
+ }
1313
+ }
1314
+ }
1315
+ } catch {
1316
+ // Best-effort — spec wiring failure must not break legacy providers.
1317
+ }
1318
+ }
1319
+
1009
1320
  return resolved;
1010
1321
  }
1011
1322
 
@@ -1016,16 +1327,27 @@ export class ProviderLoader {
1016
1327
  private loadScriptsFromDir(type: string, scriptDir: string): Partial<ProviderScripts> | null {
1017
1328
  const providerDir = this.findProviderDirInternal(type);
1018
1329
  if (!providerDir) {
1019
- this.log(` [loadScriptsFromDir] ${type}: providerDir not found`);
1330
+ // No provider dir for this type a spec-only provider with no
1331
+ // legacy scripts/v1 layout is a normal configuration, not a
1332
+ // problem to surface at INFO. resolve() calls this on every
1333
+ // request; INFO spam every 200ms drowns out the real signal.
1334
+ this.debugLog(`[loadScriptsFromDir] ${type}: providerDir not found`);
1020
1335
  return null;
1021
1336
  }
1022
1337
 
1023
1338
  const dir = path.join(providerDir, scriptDir);
1024
1339
  if (!fs.existsSync(dir)) {
1025
- this.log(` [loadScriptsFromDir] ${type}: dir not found: ${dir}`);
1340
+ this.debugLog(`[loadScriptsFromDir] ${type}: dir not found: ${dir}`);
1026
1341
  return null;
1027
1342
  }
1028
1343
 
1344
+ // Register the provider's *parent root* (e.g. .../adhdev-providers/) so
1345
+ // the require whitelist gates every script + every _shared helper this
1346
+ // provider may reach. Picking the grandparent (one above the category
1347
+ // dir `cli/`) lets sibling helpers in `_shared` resolve while still
1348
+ // blocking `../../etc/...` escapes. Idempotent.
1349
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1350
+
1029
1351
  // Return cached scripts if available (cleared on reload/watch)
1030
1352
  const cached = this.scriptsCache.get(dir);
1031
1353
  if (cached) return cached;
@@ -1036,7 +1358,7 @@ export class ProviderLoader {
1036
1358
  try {
1037
1359
  delete require.cache[require.resolve(scriptsJs)];
1038
1360
  const loaded = require(scriptsJs);
1039
- this.log(` [loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
1361
+ this.debugLog(`[loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
1040
1362
  this.scriptsCache.set(dir, loaded);
1041
1363
  return loaded;
1042
1364
  } catch (e) {
@@ -1067,13 +1389,17 @@ export class ProviderLoader {
1067
1389
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
1068
1390
  });
1069
1391
 
1392
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
1070
1393
  const handleChange = (filePath: string) => {
1071
1394
  if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
1072
1395
  return;
1073
1396
  }
1074
1397
  if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
1075
- this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1076
- this.reload();
1398
+ if (reloadTimer) clearTimeout(reloadTimer);
1399
+ reloadTimer = setTimeout(() => {
1400
+ this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1401
+ this.reload();
1402
+ }, 300);
1077
1403
  }
1078
1404
  };
1079
1405
 
@@ -1124,13 +1450,118 @@ export class ProviderLoader {
1124
1450
  *
1125
1451
  * @returns Whether an update occurred
1126
1452
  */
1453
+ /**
1454
+ * Sync providers from the ADHDev registry (registry.adhf.dev).
1455
+ *
1456
+ * Downloads only providers whose server checksum differs from the locally
1457
+ * cached checksum. Falls back gracefully to the GitHub tarball path if the
1458
+ * registry is unreachable or returns an unexpected response.
1459
+ *
1460
+ * Returns `{ updated: true }` when at least one provider file changed on disk,
1461
+ * `{ updated: false }` when everything is already current, or
1462
+ * `{ updated: false, error }` when the registry couldn't be reached and we
1463
+ * should proceed to the GitHub tarball fallback.
1464
+ */
1465
+ async fetchFromRegistry(): Promise<{ updated: boolean; error?: string }> {
1466
+ if (this.disableUpstream) {
1467
+ this.log('Registry sync skipped (sourceMode=no-upstream)');
1468
+ return { updated: false };
1469
+ }
1470
+ this.log(`Registry sync starting (${ProviderLoader.REGISTRY_BASE_URL})...`);
1471
+
1472
+ const https = require('https') as typeof import('https');
1473
+ const regMetaPath = path.join(this.upstreamDir, ProviderLoader.REGISTRY_META_FILE);
1474
+
1475
+ // Load cached checksums
1476
+ let cachedChecksums: Record<string, string> = {};
1477
+ try {
1478
+ if (fs.existsSync(regMetaPath)) {
1479
+ cachedChecksums = JSON.parse(fs.readFileSync(regMetaPath, 'utf-8')).checksums ?? {};
1480
+ }
1481
+ } catch { }
1482
+
1483
+ try {
1484
+ // 1. Fetch provider list
1485
+ const listUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers`;
1486
+ const listBody = await new Promise<string>((resolve, reject) => {
1487
+ const req = https.get(listUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
1488
+ if (res.statusCode !== 200) { reject(new Error(`registry list HTTP ${res.statusCode}`)); return; }
1489
+ const chunks: Buffer[] = [];
1490
+ res.on('data', (c: Buffer) => chunks.push(c));
1491
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1492
+ });
1493
+ req.on('error', reject);
1494
+ req.on('timeout', () => { req.destroy(); reject(new Error('registry list timeout')); });
1495
+ });
1496
+
1497
+ const list = JSON.parse(listBody) as { providers: Array<{ type: string; category: string; checksum: string; version: string }> };
1498
+ if (!Array.isArray(list.providers)) throw new Error('unexpected registry response shape');
1499
+
1500
+ let updatedCount = 0;
1501
+
1502
+ for (const entry of list.providers) {
1503
+ const { type, category, checksum, version } = entry;
1504
+ const cacheKey = `${category}/${type}`;
1505
+ if (cachedChecksums[cacheKey] === checksum) continue; // already current
1506
+
1507
+ // Download this provider's manifest
1508
+ const dlUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers/${type}/${version}/download`;
1509
+ const manifestBody = await new Promise<string>((resolve, reject) => {
1510
+ const req = https.get(dlUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 30000 }, (res) => {
1511
+ if (res.statusCode !== 200) { reject(new Error(`registry download HTTP ${res.statusCode} for ${type}@${version}`)); return; }
1512
+ const chunks: Buffer[] = [];
1513
+ res.on('data', (c: Buffer) => chunks.push(c));
1514
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1515
+ });
1516
+ req.on('error', reject);
1517
+ req.on('timeout', () => { req.destroy(); reject(new Error(`download timeout for ${type}`)); });
1518
+ });
1519
+
1520
+ // Verify checksum
1521
+ const actualChecksum = await new Promise<string>((resolve) => {
1522
+ const crypto = require('crypto') as typeof import('crypto');
1523
+ resolve(crypto.createHash('sha256').update(manifestBody, 'utf-8').digest('hex'));
1524
+ });
1525
+ if (actualChecksum !== checksum) {
1526
+ this.log(`⚠ Registry checksum mismatch for ${type}@${version} — skipping`);
1527
+ continue;
1528
+ }
1529
+
1530
+ // Write to upstream dir
1531
+ const providerDir = path.join(this.upstreamDir, category, type);
1532
+ fs.mkdirSync(providerDir, { recursive: true });
1533
+ fs.writeFileSync(path.join(providerDir, 'provider.json'), manifestBody, 'utf-8');
1534
+
1535
+ cachedChecksums[cacheKey] = checksum;
1536
+ updatedCount++;
1537
+ this.log(`✓ Registry updated: ${category}/${type}@${version}`);
1538
+ }
1539
+
1540
+ // Persist updated checksums
1541
+ fs.mkdirSync(this.upstreamDir, { recursive: true });
1542
+ fs.writeFileSync(regMetaPath, JSON.stringify({
1543
+ checksums: cachedChecksums,
1544
+ syncedAt: new Date().toISOString(),
1545
+ providerCount: list.providers.length,
1546
+ }, null, 2));
1547
+
1548
+ this.log(`Registry sync complete: ${list.providers.length} providers, ${updatedCount} updated`);
1549
+ return { updated: updatedCount > 0 };
1550
+ } catch (e: any) {
1551
+ this.log(`⚠ Registry sync failed (falling back to GitHub tarball): ${e?.message}`);
1552
+ return { updated: false, error: e?.message };
1553
+ }
1554
+ }
1555
+
1127
1556
  async fetchLatest(): Promise<{ updated: boolean; error?: string }> {
1128
1557
  if (this.disableUpstream) {
1129
1558
  this.log('Upstream fetch skipped (sourceMode=no-upstream)');
1130
1559
  return { updated: false };
1131
1560
  }
1132
1561
  const https = require('https') as typeof import('https');
1133
- const { execSync } = require('child_process') as typeof import('child_process');
1562
+ const { exec } = require('child_process') as typeof import('child_process');
1563
+ const { promisify } = require('util');
1564
+ const execAsync = promisify(exec);
1134
1565
 
1135
1566
  const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
1136
1567
  let prevEtag = '';
@@ -1207,7 +1638,7 @@ export class ProviderLoader {
1207
1638
 
1208
1639
  // Extract
1209
1640
  fs.mkdirSync(tmpExtract, { recursive: true });
1210
- execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1641
+ await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1211
1642
 
1212
1643
  // Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
1213
1644
  const extracted = fs.readdirSync(tmpExtract);
@@ -1315,15 +1746,17 @@ export class ProviderLoader {
1315
1746
  } catch { }
1316
1747
  }
1317
1748
 
1318
- /** Count provider files (provider.js or provider.json) */
1749
+ /** Count provider files (provider.v1.json or provider.json — at most one per dir). */
1319
1750
  private countProviders(dir: string): number {
1320
1751
  if (!fs.existsSync(dir)) return 0;
1321
1752
  let count = 0;
1322
1753
  const scan = (d: string) => {
1323
1754
  try {
1324
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
1755
+ const entries = fs.readdirSync(d, { withFileTypes: true });
1756
+ const hasManifest = entries.some(e => e.name === 'provider.v1.json' || e.name === 'provider.json');
1757
+ if (hasManifest) count++;
1758
+ for (const entry of entries) {
1325
1759
  if (entry.isDirectory()) scan(path.join(d, entry.name));
1326
- else if (entry.name === 'provider.json') count++;
1327
1760
  }
1328
1761
  } catch { }
1329
1762
  };
@@ -1372,11 +1805,15 @@ export class ProviderLoader {
1372
1805
  return args ? args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(' ') : '';
1373
1806
  }
1374
1807
  const schemaDef = this.getSettingsSchema(providerType)[key];
1375
- const defaultVal = schemaDef
1376
- ? (key === 'autoApprove' && schemaDef.type === 'boolean'
1377
- ? true
1378
- : schemaDef.default)
1379
- : undefined;
1808
+ // (fix) Previously this hard-coded `autoApprove` boolean default to `true`,
1809
+ // overriding whatever schemaDef.default the provider.json declared. That
1810
+ // surfaced as soon as a provider added an `autoApprove` schema entry with
1811
+ // default=false: the user had never opted in but the daemon treated the
1812
+ // session as auto-approve, which then triggered recordAutoApproval every
1813
+ // time the CLI showed an approval modal — producing a flood of system
1814
+ // "Auto-approved: ..." messages and keeping the session pinned to
1815
+ // generating while modals cycled in and out. Trust the schemaDef.default.
1816
+ const defaultVal = schemaDef ? schemaDef.default : undefined;
1380
1817
 
1381
1818
  const config = this.readConfig();
1382
1819
  const userVal = config?.providerSettings?.[providerType]?.[key];
@@ -1490,10 +1927,17 @@ export class ProviderLoader {
1490
1927
  ...this.getSyntheticSettings(type, provider),
1491
1928
  ...(provider.settings || {}),
1492
1929
  };
1930
+ // (fix) Previously this clause forced `autoApprove.default = true` for any
1931
+ // boolean autoApprove schema, even when the provider.json explicitly set
1932
+ // `default: false`. Combined with the synthetic-settings fallback at
1933
+ // getSyntheticSettings (which also defaults autoApprove to true when the
1934
+ // provider doesn't supply one), that meant CLI providers silently turned on
1935
+ // auto-approval, producing a flood of "Auto-approved: ..." system messages
1936
+ // every time an approval modal appeared and pinning the session to
1937
+ // generating while modals cycled. Trust the provider's declared default.
1493
1938
  if (result.autoApprove?.type === 'boolean') {
1494
1939
  result.autoApprove = {
1495
1940
  ...result.autoApprove,
1496
- default: true,
1497
1941
  public: true,
1498
1942
  label: result.autoApprove.label || 'Auto Approve',
1499
1943
  description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1518,7 +1962,10 @@ export class ProviderLoader {
1518
1962
  if (!provider.settings?.autoApprove) {
1519
1963
  result.autoApprove = {
1520
1964
  type: 'boolean',
1521
- default: true,
1965
+ // (fix) Safe default is *off*. Auto-approving every modal without the
1966
+ // user opting in produced silent-bash-execution surprises and the
1967
+ // "Auto-approved: ..." system-message flood seen on AGY/Codex.
1968
+ default: false,
1522
1969
  public: true,
1523
1970
  label: 'Auto Approve',
1524
1971
  description: 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1581,23 +2028,32 @@ export class ProviderLoader {
1581
2028
  const cat = provider.category;
1582
2029
 
1583
2030
  const searchRoots = this.getProviderRoots();
2031
+ const hasManifest = (dir: string) =>
2032
+ fs.existsSync(path.join(dir, 'provider.v1.json')) || fs.existsSync(path.join(dir, 'provider.json'));
2033
+ const readManifestType = (dir: string): string | null => {
2034
+ for (const file of ['provider.v1.json', 'provider.json']) {
2035
+ const p = path.join(dir, file);
2036
+ if (!fs.existsSync(p)) continue;
2037
+ try {
2038
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
2039
+ if (typeof data?.type === 'string') return data.type;
2040
+ } catch { /* skip */ }
2041
+ }
2042
+ return null;
2043
+ };
1584
2044
  for (const root of searchRoots) {
1585
2045
  if (!fs.existsSync(root)) continue;
1586
2046
  const candidate = this.getProviderDir(root, cat, type);
1587
- if (fs.existsSync(path.join(candidate, 'provider.json'))) return candidate;
2047
+ if (hasManifest(candidate)) return candidate;
1588
2048
  // Scan category dir for type match
1589
2049
  const catDir = path.join(root, cat);
1590
2050
  if (fs.existsSync(catDir)) {
1591
2051
  try {
1592
2052
  for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
1593
2053
  if (!entry.isDirectory()) continue;
1594
- const jsonPath = path.join(catDir, entry.name, 'provider.json');
1595
- if (fs.existsSync(jsonPath)) {
1596
- try {
1597
- const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
1598
- if (data.type === type) return path.join(catDir, entry.name);
1599
- } catch { /* skip */ }
1600
- }
2054
+ const entryDir = path.join(catDir, entry.name);
2055
+ const manifestType = readManifestType(entryDir);
2056
+ if (manifestType === type) return entryDir;
1601
2057
  }
1602
2058
  } catch { /* skip */ }
1603
2059
  }
@@ -1690,17 +2146,44 @@ export class ProviderLoader {
1690
2146
  return;
1691
2147
  }
1692
2148
 
1693
- // Check if this directory has provider.json
2149
+ // v1-first manifest selection. provider.v1.json (the SDK-shape
2150
+ // manifest with `overrides`, `tui`, `source`, `canonicalHistory`)
2151
+ // wins over provider.json (legacy). Without this branch the v1
2152
+ // file is silently ignored — that's how the codex-cli `overrides`
2153
+ // path and the tui-block builders went un-honored for the first
2154
+ // pass of SDK rollout.
2155
+ const hasV1 = entries.some(e => e.name === 'provider.v1.json');
1694
2156
  const hasJson = entries.some(e => e.name === 'provider.json');
1695
2157
 
1696
- if (hasJson) {
1697
- const jsonPath = path.join(d, 'provider.json');
2158
+ if (hasV1 || hasJson) {
2159
+ const manifestFile = hasV1 ? 'provider.v1.json' : 'provider.json';
2160
+ const jsonPath = path.join(d, manifestFile);
1698
2161
  try {
1699
2162
  const raw = fs.readFileSync(jsonPath, 'utf-8');
1700
2163
  const mod = JSON.parse(raw) as Omit<ProviderModule, 'extensionIdPattern'> & {
1701
2164
  extensionIdPattern?: RegExp | string;
1702
2165
  };
1703
2166
 
2167
+ // Validate v1 manifests against the SDK schema. Failures are
2168
+ // surfaced as a single warning line with all issues attached
2169
+ // so manifest authors don't need to guess which field is wrong.
2170
+ // Loading still proceeds — bricking the daemon on a single
2171
+ // bad field would be worse than running with a known warning.
2172
+ if (hasV1 && mod?.category === 'cli') {
2173
+ try {
2174
+ const { validateCliProviderManifest, formatManifestValidationIssues } =
2175
+ require('./sdk/v1/validators/manifest.js') as typeof import('./sdk/v1/validators/manifest.js');
2176
+ const validation = validateCliProviderManifest(mod);
2177
+ if (!validation.ok) {
2178
+ this.log(`⚠ ${jsonPath}: schema validation failed:\n${formatManifestValidationIssues(validation.issues)}`);
2179
+ }
2180
+ } catch (e: any) {
2181
+ // Validator load failed — log once and continue so a
2182
+ // broken validator can't take down provider loading.
2183
+ this.log(`⚠ ${jsonPath}: validator unavailable: ${e?.message || e}`);
2184
+ }
2185
+ }
2186
+
1704
2187
  // Restore RegExp fields from JSON (extensionIdPattern)
1705
2188
  if (typeof mod.extensionIdPattern === 'string') {
1706
2189
  const flags = mod.extensionIdPattern_flags || '';
@@ -1712,6 +2195,22 @@ export class ProviderLoader {
1712
2195
  ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1713
2196
  };
1714
2197
 
2198
+ // v1 manifests use `nativeHistory` as the canonical field name.
2199
+ // Legacy v0 manifests use `canonicalHistory`. The daemon's
2200
+ // runtime + downstream code reads `provider.nativeHistory`, so
2201
+ // for legacy manifests we copy `canonicalHistory` into
2202
+ // `nativeHistory` here. We also keep `canonicalHistory`
2203
+ // populated in both directions (deprecated alias) so any
2204
+ // external consumers still reading the old name keep working
2205
+ // during the one-release deprecation window.
2206
+ const nh = (normalizedProvider as any).nativeHistory;
2207
+ const ch = (normalizedProvider as any).canonicalHistory;
2208
+ if (nh && !ch) {
2209
+ (normalizedProvider as any).canonicalHistory = nh;
2210
+ } else if (ch && !nh) {
2211
+ (normalizedProvider as any).nativeHistory = ch;
2212
+ }
2213
+
1715
2214
  const validation = validateProviderDefinition(normalizedProvider);
1716
2215
  for (const warning of validation.warnings) {
1717
2216
  this.log(`⚠ ${jsonPath}: ${warning}`);
@@ -1725,6 +2224,10 @@ export class ProviderLoader {
1725
2224
  const scriptsPath = path.join(d, 'scripts.js');
1726
2225
  if (!hasCompatibility && fs.existsSync(scriptsPath)) {
1727
2226
  try {
2227
+ // Gate the IDE/extension scripts.js (legacy single-file
2228
+ // format) under the same whitelist. `d` here is the
2229
+ // provider dir; its grandparent contains _shared.
2230
+ registerProviderScriptRootSafely(path.dirname(path.dirname(d)));
1728
2231
  delete require.cache[require.resolve(scriptsPath)];
1729
2232
  const scripts = require(scriptsPath) as Partial<ProviderScripts>;
1730
2233
  normalizedProvider.scripts = scripts;
@@ -1733,14 +2236,40 @@ export class ProviderLoader {
1733
2236
  }
1734
2237
  }
1735
2238
 
2239
+ // Classify trust based on which on-disk layer this manifest
2240
+ // came from + whether it ships JavaScript hooks. The dashboard
2241
+ // uses this to render trust badges; non-spec external manifests
2242
+ // need an explicit user confirm before activation.
2243
+ const externalDirAbs = path.join(os.homedir(), '.adhdev', 'external');
2244
+ const layer: 'user' | 'upstream' | 'external' = d.startsWith(externalDirAbs)
2245
+ ? 'external'
2246
+ : (d.startsWith(this.userDir) && !d.includes('.upstream') ? 'user' : 'upstream');
2247
+ try {
2248
+ const { inspectManifestShape, classifyTrust } =
2249
+ require('./provider-trust.js') as typeof import('./provider-trust.js');
2250
+ const shape = inspectManifestShape(mod as Record<string, unknown>);
2251
+ const trust = classifyTrust(layer, shape);
2252
+ (normalizedProvider as any)._sourceLayer = layer;
2253
+ (normalizedProvider as any)._sourceTrust = trust;
2254
+ (normalizedProvider as any)._manifestShape = shape;
2255
+ // For external-namespaced layouts (external/<source>/…) record
2256
+ // which source the manifest came from so dashboards can name
2257
+ // it in the trust badge.
2258
+ if (layer === 'external') {
2259
+ const rel = path.relative(externalDirAbs, d);
2260
+ const firstSeg = rel.split(path.sep)[0];
2261
+ if (firstSeg && firstSeg !== '..') (normalizedProvider as any)._sourceName = firstSeg;
2262
+ }
2263
+ } catch { /* best-effort — trust is enrichment, not gating */ }
2264
+
1736
2265
  const existed = this.providers.has(normalizedProvider.type);
1737
2266
  this.providers.set(normalizedProvider.type, normalizedProvider);
1738
2267
  count++;
1739
- // Identify source tier for debugging
1740
- const source = d.startsWith(this.userDir) && !d.includes('.upstream')
1741
- ? 'user' : 'upstream';
2268
+ const source = (normalizedProvider as any)._sourceLayer ?? 'upstream';
1742
2269
  const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES upstream' : '';
1743
- this.log(` ${existed ? '🔄' : '✅'} ${normalizedProvider.type} (${normalizedProvider.category}) ${normalizedProvider.name} [${source}]${overrideWarning}`);
2270
+ const sourceName = (normalizedProvider as any)._sourceName;
2271
+ const sourceLabel = sourceName ? `${source}/${sourceName}` : source;
2272
+ this.log(` ${existed ? '🔄' : '✅'} ${normalizedProvider.type} (${normalizedProvider.category}) — ${normalizedProvider.name} [${sourceLabel}]${overrideWarning}`);
1744
2273
  }
1745
2274
  } catch (e) {
1746
2275
  this.log(`⚠ Failed to load ${jsonPath}: ${(e as Error).message}`);
@@ -1752,6 +2281,11 @@ export class ProviderLoader {
1752
2281
  for (const entry of entries) {
1753
2282
  if (!entry.isDirectory()) continue;
1754
2283
  if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
2284
+ // `examples/` is a documentation / scaffold tree (e.g. stub-cli),
2285
+ // not a real provider source. SDK authors copy from here when
2286
+ // writing a new provider; daemon-core tests reference the
2287
+ // manifest by path. Keep it off the dashboard's provider list.
2288
+ if (d === dir && entry.name === 'examples') continue;
1755
2289
  if (excludeDirs && d === dir && excludeDirs.includes(entry.name)) continue;
1756
2290
  scan(path.join(d, entry.name));
1757
2291
  }