@adhdev/daemon-core 0.9.82-rc.22 → 0.9.82-rc.220

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