@adhdev/daemon-core 0.9.82-rc.456 → 0.9.82-rc.458

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.
@@ -14,6 +14,22 @@ export interface DebugRuntimeConfig {
14
14
  traceBufferSize: number;
15
15
  traceCategories: string[];
16
16
  }
17
+ /**
18
+ * ALWAYS-ON trace categories. These bypass the `collectDebugTrace` master switch
19
+ * (and category selection) so they are collected in production daemons where
20
+ * `--trace` is unset. They exist so mesh completion diagnostics — the FSM-transition
21
+ * and completion-gate snapshots that explain an early / missing agent:generating_completed
22
+ * notification — are retrievable via mesh_read_debug (chat_debug_bundle) without asking
23
+ * an operator to relaunch the daemon with tracing on.
24
+ *
25
+ * SAFETY: only add a category here after confirming every record() call site for it
26
+ * carries a content-free payload (statuses, epochs, timestamps, deltas, lengths, roles,
27
+ * enum-like reasons — never transcript / prompt / bubble text). Always-on collection makes
28
+ * such payloads unconditional, so a content-bearing field would leak into the ring buffer
29
+ * in production.
30
+ */
31
+ export declare const ALWAYS_ON_TRACE_CATEGORIES: readonly string[];
32
+ export declare function isAlwaysOnTraceCategory(category?: string | null): boolean;
17
33
  export declare function resolveDebugRuntimeConfig(options?: DebugRuntimeOptions): DebugRuntimeConfig;
18
34
  export declare function setDebugRuntimeConfig(config: DebugRuntimeConfig): void;
19
35
  export declare function getDebugRuntimeConfig(): DebugRuntimeConfig;
@@ -15,4 +15,14 @@ export interface PreviewFreshness {
15
15
  }>;
16
16
  nextAction: string;
17
17
  }
18
- export declare function buildPreviewFreshness(repoRoot: string): PreviewFreshness;
18
+ /**
19
+ * Gate: does this repository actually configure the preview-deploy pipeline?
20
+ *
21
+ * The preview-freshness surface embeds this project's private release-pipeline
22
+ * instructions (`npm run deploy:preview`, smoke preview, …). Those are only
23
+ * meaningful in a repo that ships the pipeline. An external repo joined to a
24
+ * mesh must not have that guidance leak into its coordinator prompt, so this
25
+ * gate keeps the surface off unless a concrete pipeline artifact is present.
26
+ */
27
+ export declare function isPreviewPipelineConfigured(repoRoot: string): boolean;
28
+ export declare function buildPreviewFreshness(repoRoot: string): PreviewFreshness | null;
@@ -29,6 +29,60 @@ export interface WorktreeBootstrapState extends MeshAsyncJobLifecycle {
29
29
  staleReason?: string;
30
30
  }
31
31
  export declare const WORKTREE_BOOTSTRAP_STALE_RUNNING_MS: number;
32
+ /**
33
+ * Enumerate registered submodule paths for a worktree, generically (no hardcoded
34
+ * 'oss'/'adhdev-providers'). Reads `.gitmodules` via `git config --file .gitmodules
35
+ * --get-regexp path`, whose lines are `submodule.<name>.path <relativePath>`. Paths
36
+ * are returned normalized to forward slashes (git porcelain always emits '/'),
37
+ * trailing slash stripped. Returns an empty set when there are no submodules or the
38
+ * lookup fails — callers must then treat any change as dirty (conservative).
39
+ */
40
+ export declare function getRegisteredSubmodulePaths(workspace: string): Set<string>;
41
+ /**
42
+ * Read each registered submodule's configured `branch` from `.gitmodules`, keyed
43
+ * by the submodule's normalized path (matching {@link getRegisteredSubmodulePaths}).
44
+ *
45
+ * `.gitmodules` stores `submodule.<name>.path` and (optionally)
46
+ * `submodule.<name>.branch`; this joins the two on `<name>`. The special branch
47
+ * value `.` ("track the superproject's branch") is deliberately OMITTED so callers
48
+ * fall through to remote-HEAD detection instead of treating `.` as a literal branch
49
+ * name. Returns an empty map when there are no submodules, no `.gitmodules`, or the
50
+ * lookup fails (conservative — callers then detect or fall back).
51
+ */
52
+ export declare function getSubmoduleConfiguredBranches(workspace: string): Map<string, string>;
53
+ /** Fallback submodule branch when no configured/detected default can be resolved. */
54
+ export declare const SUBMODULE_DEFAULT_BRANCH_FALLBACK = "main";
55
+ /**
56
+ * Resolve the default branch a submodule's commits are published to / checked for
57
+ * reachability against. Generalizes the previously hardcoded `main` so a submodule
58
+ * whose default branch is `master`/`trunk`/etc. is handled. Priority (each tier
59
+ * falls through to the next on miss/error):
60
+ *
61
+ * 1. `.gitmodules` `submodule.<name>.branch` (via {@link getSubmoduleConfiguredBranches};
62
+ * `.` is ignored) — an explicit, local, zero-cost declaration.
63
+ * 2. the submodule checkout's LOCAL remote HEAD: `git symbolic-ref --short
64
+ * refs/remotes/<remote>/HEAD` → strip the `<remote>/` prefix (no network).
65
+ * 3. the submodule remote's advertised HEAD: `git ls-remote --symref <remote> HEAD`
66
+ * → `ref: refs/heads/<branch>` (one network round-trip).
67
+ * 4. fallback {@link SUBMODULE_DEFAULT_BRANCH_FALLBACK} (`'main'`).
68
+ *
69
+ * Because the final fallback is `'main'` and every earlier tier that resolves `'main'`
70
+ * yields the same string, a repo whose submodules default to `main` (the common case)
71
+ * produces byte-identical downstream fetch/merge-base/push ref targets — only a
72
+ * read-only resolution probe is added.
73
+ */
74
+ export declare function resolveSubmoduleDefaultBranch(opts: {
75
+ /** The submodule's local checkout — cwd for symbolic-ref / ls-remote. */
76
+ submoduleRepoPath: string;
77
+ /** The superproject workspace — for the `.gitmodules` branch lookup (tier 1). */
78
+ superprojectWorkspace?: string;
79
+ /** The submodule's path relative to the superproject (key into `.gitmodules`). */
80
+ submodulePath?: string;
81
+ /** Remote name (default `origin`). */
82
+ remote?: string;
83
+ /** Timeout for the local probe (tier 2); the network probe (tier 3) gets max(this, 30s). */
84
+ timeoutMs?: number;
85
+ }): Promise<string>;
32
86
  export declare function isWorktreeBootstrapStaleRunning(node: {
33
87
  worktreeBootstrap?: {
34
88
  status?: string;
@@ -299,6 +299,10 @@ export declare class CliProviderInstance implements ProviderInstance {
299
299
  */
300
300
  private completingTurnTaskId;
301
301
  private meshTraceCtx;
302
+ private completionTraceOn;
303
+ private fsmTraceOn;
304
+ private recordCompletionGateTrace;
305
+ private recordFsmTransitionTrace;
302
306
  private flushCompletedDebounceIfFinalized;
303
307
  private maybeAutoApproveStatus;
304
308
  /**
@@ -55,10 +55,15 @@ export declare class ProviderLoader {
55
55
  private logFn;
56
56
  private versionArchive;
57
57
  private scriptsCache;
58
+ /**
59
+ * Resolved registry base URL and provider tarball URL. Resolution order:
60
+ * explicit config field (constructor option) → env var → vendor default.
61
+ * See `config/registry-resolver.ts`.
62
+ */
63
+ private readonly registryBaseUrl;
64
+ private readonly providerTarballUrl;
58
65
  /** Inject VersionArchive so resolve() can auto-detect installed versions */
59
66
  setVersionArchive(archive: VersionArchive): void;
60
- private static readonly GITHUB_TARBALL_URL;
61
- private static readonly REGISTRY_BASE_URL;
62
67
  private static readonly META_FILE;
63
68
  private static readonly REGISTRY_META_FILE;
64
69
  private static readonly REPO_PROVIDER_DIRNAME;
@@ -85,6 +90,16 @@ export declare class ProviderLoader {
85
90
  * probing; production code should leave this unset.
86
91
  */
87
92
  probeStarts?: string[];
93
+ /**
94
+ * Explicit provider registry base URL override (config.registryUrl).
95
+ * Highest-priority resolver source, ahead of ADHDEV_REGISTRY_URL + default.
96
+ */
97
+ registryUrl?: string;
98
+ /**
99
+ * Explicit provider tarball URL override (config.providerTarballUrl).
100
+ * Highest-priority resolver source, ahead of ADHDEV_PROVIDER_TARBALL_URL + default.
101
+ */
102
+ providerTarballUrl?: string;
88
103
  });
89
104
  private migrateMarketplaceDirToExternal;
90
105
  private log;
@@ -181,6 +181,11 @@ export declare class FsmDriver implements ISpecDriver {
181
181
  * (−1 = whole screen), or a `section:<id>` / `<region>#ignore:<pat>` string
182
182
  * when the clause scopes to a section or declares an ignore_lines filter. */
183
183
  private regionLastChangedAt;
184
+ /** COMPLETION-EARLYNOTIFY stable-eval trace: last stable/not-stable verdict
185
+ * recorded per stable region, so the trace fires only when the verdict FLIPS
186
+ * (not every quiet frame). Cleared on every transition alongside
187
+ * regionLastChangedAt. Diagnostic-only — never consulted by the FSM. */
188
+ private stableVerdictCache;
184
189
  /** Timer that re-runs evaluate() when a time-condition would flip true
185
190
  * with no PTY frame to trigger it. */
186
191
  private wakeTimer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.456",
3
+ "version": "0.9.82-rc.458",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,8 +46,8 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.456",
50
- "@adhdev/session-host-core": "0.9.82-rc.456",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.458",
50
+ "@adhdev/session-host-core": "0.9.82-rc.458",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
53
53
  "ajv-formats": "^3.0.1",
@@ -180,6 +180,8 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
180
180
  logFn: config.providerLogFn,
181
181
  sourceMode: providerSourceMode,
182
182
  userDir: appConfig.providerDir,
183
+ registryUrl: appConfig.registryUrl,
184
+ providerTarballUrl: appConfig.providerTarballUrl,
183
185
  });
184
186
 
185
187
  // Boot-time auto-sync is intentionally disabled. The user picks which
@@ -20,6 +20,7 @@ import type { ProviderModule, ProviderScripts } from '../providers/contracts.js'
20
20
  import type { DaemonAgentStreamManager } from '../agent-stream/index.js';
21
21
  import type { CliAdapter } from '../cli-adapter-types.js';
22
22
  import { loadConfig } from '../config/config.js';
23
+ import { resolveRegistryBaseUrl } from '../config/registry-resolver.js';
23
24
  import { ChatHistoryWriter } from '../config/chat-history.js';
24
25
  import type { SessionRegistry, SessionRuntimeTarget } from '../sessions/registry.js';
25
26
  import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
@@ -692,7 +693,7 @@ export class DaemonCommandHandler implements CommandHelpers {
692
693
  const https = require('https') as typeof import('https');
693
694
  const fs = require('fs') as typeof import('fs');
694
695
  const path = require('path') as typeof import('path');
695
- const REGISTRY = 'https://api.adhf.dev/api/v1/registry';
696
+ const REGISTRY = resolveRegistryBaseUrl(loadConfig().registryUrl);
696
697
 
697
698
  function fetchText(url: string, timeoutMs: number): Promise<string> {
698
699
  return new Promise((resolve, reject) => {
@@ -1121,7 +1122,7 @@ export class DaemonCommandHandler implements CommandHelpers {
1121
1122
  if (!installed.success) return installed;
1122
1123
 
1123
1124
  const https = require('https') as typeof import('https');
1124
- const REGISTRY = 'https://api.adhf.dev/api/v1/registry';
1125
+ const REGISTRY = resolveRegistryBaseUrl(loadConfig().registryUrl);
1125
1126
 
1126
1127
  function fetchJson(url: string): Promise<any> {
1127
1128
  return new Promise((resolve, reject) => {
@@ -19,6 +19,10 @@ const CHANNEL_SERVER_URL: Record<ReleaseChannel, string> = {
19
19
  stable: 'https://api.adhf.dev',
20
20
  preview: 'https://api-preview.adhf.dev',
21
21
  };
22
+ // Vendor-managed serverUrls. A serverUrl equal to one of these (or unset) is
23
+ // steered to the channel default on upgrade; any other value is a user-set
24
+ // custom/self-host URL that must be preserved across upgrades.
25
+ const VENDOR_SERVER_URLS = new Set<string>(Object.values(CHANNEL_SERVER_URL));
22
26
 
23
27
  function normalizeReleaseChannel(value: unknown): ReleaseChannel | null {
24
28
  if (typeof value !== 'string') return null;
@@ -51,7 +55,16 @@ export const daemonLifecycleHandlers: Record<string, LowFamilyHandler> = {
51
55
  // Check channel-pinned dist-tag and resolve it to a concrete install version.
52
56
  const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
53
57
  LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
54
- updateConfig({ updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any);
58
+ // Only steer serverUrl to the channel's vendor default when the current
59
+ // value is unset or already a vendor default. A self-hoster's custom
60
+ // serverUrl must survive the upgrade instead of being clobbered.
61
+ const currentServerUrl = typeof loadConfig().serverUrl === 'string' ? loadConfig().serverUrl.trim() : '';
62
+ const useVendorServerUrl = currentServerUrl === '' || VENDOR_SERVER_URLS.has(currentServerUrl);
63
+ updateConfig(
64
+ useVendorServerUrl
65
+ ? { updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any
66
+ : { updateChannel: channel } as any,
67
+ );
55
68
  let currentInstalled: string | null = null;
56
69
  try {
57
70
  const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
@@ -12,6 +12,7 @@
12
12
  import { daemonIdsEquivalent, meshNodeIdMatches, normalizeMeshNodeId } from '@adhdev/mesh-shared';
13
13
  import { resolveMeshHostStatus, normalizeMeshDaemonRole } from '../../mesh/mesh-host-ownership.js';
14
14
  import {
15
+ getRegisteredSubmodulePaths,
15
16
  loadMeshWorktreeBootstrapConfig,
16
17
  runMeshWorktreeBootstrap,
17
18
  type WorktreeBootstrapState,
@@ -81,6 +82,83 @@ export async function decideOssCloneSync(
81
82
  return 'skip_diverged';
82
83
  }
83
84
 
85
+ /**
86
+ * Sync every registered submodule of a freshly-cloned worktree to its clone source
87
+ * node's working submodule HEAD, applying decideOssCloneSync's origin-tip-priority
88
+ * rewind guard per submodule.
89
+ *
90
+ * Generic over the submodule set: the paths come from `.gitmodules` via
91
+ * getRegisteredSubmodulePaths, so this operates identically over EVERY registered
92
+ * submodule (oss, adhdev-providers, …) instead of a hardcoded 'oss' literal. In a
93
+ * repo whose only synced submodule is `oss` the emitted git commands are byte-identical
94
+ * to the original oss-only path.
95
+ *
96
+ * Best-effort by design: a failure on one submodule is logged and skipped; it never
97
+ * blocks the other submodules or the clone. A submodule is only ever advanced to a
98
+ * STRICTLY-NEWER source SHA — the fresh (origin/main-derived) worktree tip is never
99
+ * rewound onto a behind/diverged source.
100
+ */
101
+ export async function syncClonedWorktreeSubmodules(
102
+ worktreePath: string,
103
+ sourceWorkspace: string,
104
+ rg: (ctx: GitRepoIdentity, argv: string[], opts?: { timeoutMs?: number }) => Promise<unknown>,
105
+ ): Promise<void> {
106
+ const submodulePaths = getRegisteredSubmodulePaths(worktreePath);
107
+ if (submodulePaths.size === 0) return;
108
+
109
+ const sourceCtx: GitRepoIdentity = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
110
+ const worktreeCtx: GitRepoIdentity = { workspace: worktreePath, repoRoot: worktreePath, isGitRepo: true };
111
+ const readStdout = (out: unknown): string =>
112
+ (typeof out === 'string' ? out : (out as any)?.stdout ?? '').trim();
113
+
114
+ for (const submodulePath of submodulePaths) {
115
+ try {
116
+ // Read the source node's working submodule SHA.
117
+ const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', submodulePath], { timeoutMs: 10000 });
118
+ const sourceSha = readStdout(sourceStatusOut).match(/^[+\- ]?([0-9a-f]{40})/)?.[1];
119
+ if (!sourceSha) continue;
120
+
121
+ // Read the worktree's freshly-checked-out submodule HEAD.
122
+ const subCtx: GitRepoIdentity = {
123
+ workspace: `${worktreePath}/${submodulePath}`,
124
+ repoRoot: `${worktreePath}/${submodulePath}`,
125
+ isGitRepo: true,
126
+ };
127
+ const worktreeSubSha = readStdout(await rg(subCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 }));
128
+ if (!worktreeSubSha || worktreeSubSha === sourceSha) continue;
129
+
130
+ // Bring the source node's submodule HEAD into the worktree submodule object
131
+ // DB so both SHAs are resolvable for the ancestry (rewind) guard below.
132
+ await rg(subCtx, ['fetch', `${sourceWorkspace}/${submodulePath}`, 'HEAD'], { timeoutMs: 60000 });
133
+
134
+ // Rewind guard: the worktree submodule HEAD was just checked out from the
135
+ // FRESH (origin/main-derived) root base. Only advance to the source SHA when
136
+ // it is strictly newer — never rewind to a stale source.
137
+ let action: OssCloneSyncAction;
138
+ try {
139
+ action = await decideOssCloneSync(subCtx, worktreeSubSha, sourceSha, rg);
140
+ } catch (decideErr: any) {
141
+ action = 'skip_diverged';
142
+ console.warn(`[mesh] ${submodulePath} submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
143
+ }
144
+
145
+ if (action === 'advance') {
146
+ await rg(subCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
147
+ await rg(worktreeCtx, ['add', submodulePath], { timeoutMs: 10000 });
148
+ await rg(worktreeCtx, ['commit', '-m', `chore: sync ${submodulePath} to source node HEAD on clone`], { timeoutMs: 10000 });
149
+ console.log(`[mesh] Advanced ${submodulePath} submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
150
+ } else if (action === 'skip_rewind') {
151
+ console.warn(`[mesh] Skipped ${submodulePath} submodule rewind on clone: source node ${submodulePath} ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree ${submodulePath} ${worktreeSubSha.slice(0, 8)} — kept fresher worktree HEAD`);
152
+ } else if (action === 'skip_diverged') {
153
+ console.warn(`[mesh] Skipped ${submodulePath} submodule sync on clone: source node ${submodulePath} ${sourceSha.slice(0, 8)} diverged from the fresh worktree ${submodulePath} ${worktreeSubSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
154
+ }
155
+ } catch (subErr: any) {
156
+ // Per-submodule best-effort: never let one submodule's failure block the rest.
157
+ console.warn(`[mesh] ${submodulePath} submodule sync to source HEAD failed (best-effort):`, subErr?.message ?? subErr);
158
+ }
159
+ }
160
+ }
161
+
84
162
  export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
85
163
  list_meshes: async (ctx: MedFamilyContext, _args: any) => {
86
164
  try {
@@ -1019,57 +1097,13 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
1019
1097
  );
1020
1098
  submodulesInitialized = true;
1021
1099
 
1022
- // Sync oss submodule to source node HEAD (best-effort)
1100
+ // Sync every registered submodule to the clone source node's
1101
+ // working HEAD (best-effort, generic over .gitmodules — no
1102
+ // hardcoded 'oss'; the rewind guard is applied per submodule).
1023
1103
  const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
1024
1104
  if (sourceWorkspace) {
1025
- try {
1026
- const { runGit: rg } = await import('../../git/git-executor.js');
1027
- const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
1028
- const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
1029
-
1030
- // Read source node's oss submodule SHA
1031
- const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
1032
- const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
1033
- const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
1034
- const sourceSha = sourceShaMatch?.[1];
1035
-
1036
- if (sourceSha) {
1037
- // Read worktree's current oss HEAD
1038
- const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
1039
- const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
1040
- const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
1041
-
1042
- if (worktreeOssSha && worktreeOssSha !== sourceSha) {
1043
- // Bring the source node's oss HEAD into the worktree object DB so
1044
- // both SHAs are resolvable for the ancestry (rewind) guard below.
1045
- await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
1046
-
1047
- // Rewind guard: the worktree oss HEAD was just checked out from the
1048
- // FRESH (origin/main-derived) root base. Only advance to the source
1049
- // SHA when it is strictly newer — never rewind to a stale source.
1050
- let ossAction: OssCloneSyncAction;
1051
- try {
1052
- ossAction = await decideOssCloneSync(ossCtx, worktreeOssSha, sourceSha, rg);
1053
- } catch (decideErr: any) {
1054
- ossAction = 'skip_diverged';
1055
- console.warn(`[mesh] oss submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
1056
- }
1057
-
1058
- if (ossAction === 'advance') {
1059
- await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
1060
- await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
1061
- await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
1062
- console.log(`[mesh] Advanced oss submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
1063
- } else if (ossAction === 'skip_rewind') {
1064
- console.warn(`[mesh] Skipped oss submodule rewind on clone: source node oss ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept fresher worktree HEAD`);
1065
- } else if (ossAction === 'skip_diverged') {
1066
- console.warn(`[mesh] Skipped oss submodule sync on clone: source node oss ${sourceSha.slice(0, 8)} diverged from the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
1067
- }
1068
- }
1069
- }
1070
- } catch (ossErr: any) {
1071
- console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
1072
- }
1105
+ const { runGit: rg } = await import('../../git/git-executor.js');
1106
+ await syncClonedWorktreeSubmodules(result.worktreePath, sourceWorkspace, rg);
1073
1107
  }
1074
1108
  } catch (subErr: any) {
1075
1109
  // Submodule init is best-effort; don't fail the clone
@@ -123,6 +123,22 @@ export interface ADHDevConfig {
123
123
  // Optional explicit provider override root (for example a local adhdev-providers checkout)
124
124
  providerDir?: string;
125
125
 
126
+ /**
127
+ * Optional provider registry base URL override (for example a self-hosted
128
+ * registry). Highest-priority source in the registry resolver, ahead of the
129
+ * `ADHDEV_REGISTRY_URL` env var and the vendor default. See
130
+ * `config/registry-resolver.ts`.
131
+ */
132
+ registryUrl?: string;
133
+
134
+ /**
135
+ * Optional provider tarball (archive) URL override so self-hosters can point
136
+ * the daemon at their own provider mirror instead of the vendor GitHub repo.
137
+ * Highest-priority source, ahead of `ADHDEV_PROVIDER_TARBALL_URL` and the
138
+ * vendor default. See `config/registry-resolver.ts`.
139
+ */
140
+ providerTarballUrl?: string;
141
+
126
142
  /** Preferred daemon update channel. Defaults to stable/latest. */
127
143
  updateChannel?: ReleaseChannel;
128
144
 
@@ -233,6 +249,8 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
233
249
  ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
234
250
  providerSourceMode: resolveProviderSourceMode(parsed.providerSourceMode, parsed.disableUpstream),
235
251
  providerDir: asOptionalString(parsed.providerDir),
252
+ registryUrl: asOptionalString(parsed.registryUrl),
253
+ providerTarballUrl: asOptionalString(parsed.providerTarballUrl),
236
254
  updateChannel: parsed.updateChannel === 'preview' ? 'preview' : 'stable',
237
255
  terminalSizingMode: parsed.terminalSizingMode === 'fit' ? 'fit' : 'measured',
238
256
  };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Registry / provider-distribution URL resolver.
3
+ *
4
+ * Single source of truth for the ADHDev provider *registry* base URL and the
5
+ * provider *tarball* (GitHub) URL. Self-hosters can repoint both away from the
6
+ * vendor defaults so the daemon never phones home to `api.adhf.dev` /
7
+ * `github.com/vilmire/adhdev-providers`.
8
+ *
9
+ * Resolution priority (highest first):
10
+ * 1. Explicit config field — `config.registryUrl` / `config.providerTarballUrl`
11
+ * 2. Environment variable — `ADHDEV_REGISTRY_URL` / `ADHDEV_PROVIDER_TARBALL_URL`
12
+ * 3. Vendor default — existing URLs (unchanged for default users)
13
+ *
14
+ * Default users get byte-identical behavior: the vendor defaults equal the
15
+ * literals these functions replaced.
16
+ */
17
+
18
+ /** Vendor default registry base URL (no trailing slash). */
19
+ export const DEFAULT_REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
20
+
21
+ /** Vendor default provider tarball URL (GitHub main branch archive). */
22
+ export const DEFAULT_PROVIDER_TARBALL_URL =
23
+ 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
24
+
25
+ /** Env var that overrides the registry base URL. */
26
+ export const REGISTRY_URL_ENV_VAR = 'ADHDEV_REGISTRY_URL';
27
+
28
+ /** Env var that overrides the provider tarball URL. */
29
+ export const PROVIDER_TARBALL_URL_ENV_VAR = 'ADHDEV_PROVIDER_TARBALL_URL';
30
+
31
+ function cleanString(value: unknown): string | undefined {
32
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
33
+ }
34
+
35
+ function stripTrailingSlashes(url: string): string {
36
+ return url.replace(/\/+$/, '');
37
+ }
38
+
39
+ /**
40
+ * Resolve the provider registry base URL.
41
+ *
42
+ * @param configuredUrl explicit config value (config.registryUrl); highest priority.
43
+ * @param env process env source (defaults to process.env; injectable for tests).
44
+ * @returns the resolved base URL with any trailing slash stripped, so callers
45
+ * can safely append `/providers`, `/providers/<type>`, etc.
46
+ */
47
+ export function resolveRegistryBaseUrl(
48
+ configuredUrl?: string | null,
49
+ env: NodeJS.ProcessEnv = process.env,
50
+ ): string {
51
+ const resolved =
52
+ cleanString(configuredUrl) ??
53
+ cleanString(env[REGISTRY_URL_ENV_VAR]) ??
54
+ DEFAULT_REGISTRY_BASE_URL;
55
+ return stripTrailingSlashes(resolved);
56
+ }
57
+
58
+ /**
59
+ * Resolve the provider tarball (archive) URL.
60
+ *
61
+ * @param configuredUrl explicit config value (config.providerTarballUrl); highest priority.
62
+ * @param env process env source (defaults to process.env; injectable for tests).
63
+ */
64
+ export function resolveProviderTarballUrl(
65
+ configuredUrl?: string | null,
66
+ env: NodeJS.ProcessEnv = process.env,
67
+ ): string {
68
+ return (
69
+ cleanString(configuredUrl) ??
70
+ cleanString(env[PROVIDER_TARBALL_URL_ENV_VAR]) ??
71
+ DEFAULT_PROVIDER_TARBALL_URL
72
+ );
73
+ }
74
+
75
+ /** Parsed tarball request target for callers that issue raw hostname/path requests. */
76
+ export interface TarballRequestTarget {
77
+ /** Full resolved tarball URL. */
78
+ url: string;
79
+ /** Hostname component (e.g. `github.com`). */
80
+ hostname: string;
81
+ /** Path component including any query string (e.g. `/vilmire/...tar.gz`). */
82
+ path: string;
83
+ }
84
+
85
+ /**
86
+ * Resolve the provider tarball URL and split it into `{ url, hostname, path }`
87
+ * for callers that build raw `https.request` options (e.g. HEAD ETag probes).
88
+ */
89
+ export function resolveProviderTarballTarget(
90
+ configuredUrl?: string | null,
91
+ env: NodeJS.ProcessEnv = process.env,
92
+ ): TarballRequestTarget {
93
+ const url = resolveProviderTarballUrl(configuredUrl, env);
94
+ const parsed = new URL(url);
95
+ return {
96
+ url,
97
+ hostname: parsed.hostname,
98
+ path: parsed.pathname + (parsed.search || ''),
99
+ };
100
+ }
package/src/index.ts CHANGED
@@ -394,6 +394,8 @@ export {
394
394
  getDebugRuntimeConfig,
395
395
  resetDebugRuntimeConfig,
396
396
  shouldCollectTraceCategory,
397
+ isAlwaysOnTraceCategory,
398
+ ALWAYS_ON_TRACE_CATEGORIES,
397
399
  } from './logging/debug-config.js';
398
400
  export type { DebugRuntimeOptions, DebugRuntimeConfig } from './logging/debug-config.js';
399
401
  export {
@@ -20,6 +20,26 @@ export interface DebugRuntimeConfig {
20
20
  const NORMAL_TRACE_BUFFER_SIZE = 200
21
21
  const DEV_TRACE_BUFFER_SIZE = 1000
22
22
 
23
+ /**
24
+ * ALWAYS-ON trace categories. These bypass the `collectDebugTrace` master switch
25
+ * (and category selection) so they are collected in production daemons where
26
+ * `--trace` is unset. They exist so mesh completion diagnostics — the FSM-transition
27
+ * and completion-gate snapshots that explain an early / missing agent:generating_completed
28
+ * notification — are retrievable via mesh_read_debug (chat_debug_bundle) without asking
29
+ * an operator to relaunch the daemon with tracing on.
30
+ *
31
+ * SAFETY: only add a category here after confirming every record() call site for it
32
+ * carries a content-free payload (statuses, epochs, timestamps, deltas, lengths, roles,
33
+ * enum-like reasons — never transcript / prompt / bubble text). Always-on collection makes
34
+ * such payloads unconditional, so a content-bearing field would leak into the ring buffer
35
+ * in production.
36
+ */
37
+ export const ALWAYS_ON_TRACE_CATEGORIES: readonly string[] = ['completion-gate', 'fsm-transition']
38
+
39
+ export function isAlwaysOnTraceCategory(category?: string | null): boolean {
40
+ return !!category && ALWAYS_ON_TRACE_CATEGORIES.includes(category)
41
+ }
42
+
23
43
  const DEFAULT_CONFIG: DebugRuntimeConfig = {
24
44
  logLevel: 'info',
25
45
  collectDebugTrace: false,
@@ -68,6 +88,11 @@ export function resetDebugRuntimeConfig(): void {
68
88
 
69
89
  export function shouldCollectTraceCategory(category?: string | null): boolean {
70
90
  const config = currentConfig
91
+ // Always-on categories are collected regardless of the collectDebugTrace master switch
92
+ // and regardless of any explicit traceCategories selection (they form a superset on top of
93
+ // whatever the operator requested), so an explicit --trace / --trace-categories run still
94
+ // includes them with its existing behavior unchanged.
95
+ if (isAlwaysOnTraceCategory(category)) return true
71
96
  if (!config.collectDebugTrace) return false
72
97
  if (!category) return true
73
98
  if (config.traceCategories.length === 0) return true
@@ -1,4 +1,4 @@
1
- import { getDebugRuntimeConfig, shouldCollectTraceCategory } from './debug-config.js'
1
+ import { getDebugRuntimeConfig, isAlwaysOnTraceCategory, shouldCollectTraceCategory } from './debug-config.js'
2
2
 
3
3
  export type DebugTraceLevel = 'debug' | 'info' | 'warn' | 'error'
4
4
 
@@ -80,7 +80,12 @@ export function createDebugTraceStore(options: DebugTraceStoreOptions): DebugTra
80
80
 
81
81
  return {
82
82
  record(event: DebugTraceEvent): DebugTraceEntry | null {
83
- if (!options.enabled) return null
83
+ // The store's `enabled` flag mirrors collectDebugTrace (set by configureDebugTraceStore),
84
+ // so it is false on a production daemon. Always-on categories must still land in the ring
85
+ // even then — otherwise the second gate here would swallow what shouldCollectTraceCategory
86
+ // just admitted. They share the same fixed-capacity buffer, so heavy always-on traffic can
87
+ // evict older opt-in entries; that is accepted (no separate ring).
88
+ if (!options.enabled && !isAlwaysOnTraceCategory(event.category)) return null
84
89
  const entry = createEntry(event)
85
90
  entries.push(entry)
86
91
  if (entries.length > capacity) {
@@ -577,7 +577,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
577
577
  - **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
578
578
  - **Limit parallelism.** Start with 1–2 tasks; scale only on success. Never duplicate a session because \`mesh_read_chat\` shows no final message while tool/terminal activity is ongoing. This caps *concurrent* load — it does not mean serialize independent work: when a new, independent request arrives and there is headroom under \`maxParallelTasks\`, dispatch it right away rather than waiting for an in-flight task or a user nudge (read-only diagnosis especially, since it has no merge cost).
579
579
  - **Check history first.** Call \`mesh_task_history\` at session start to avoid duplicate work and inform recovery. On failure, read task history before retrying.
580
- - **Sequence shared-base-moving merges.** Parallel dispatch is encouraged, but merging one worktree can advance another in-flight worktree's base — especially the oss submodule pointer — turning a clean fast-forward into a diverged rebase (patch-equivalence correctly blocks this). Before merging an in-flight worktree while siblings are also in flight, land in an intentional order, re-clone long-running worktrees from the advanced base, or expect to manually rebase + ff-only the laggards; merging an independent fix mid-flight can strand siblings into a rebase.
580
+ - **Sequence shared-base-moving merges.** Parallel dispatch is encouraged, but merging one worktree can advance another in-flight worktree's base — especially a shared submodule pointer — turning a clean fast-forward into a diverged rebase (patch-equivalence correctly blocks this). Before merging an in-flight worktree while siblings are also in flight, land in an intentional order, re-clone long-running worktrees from the advanced base, or expect to manually rebase + ff-only the laggards; merging an independent fix mid-flight can strand siblings into a rebase.
581
581
  - **Converge branches.** After worktree tasks: refine/fast-forward, or classify as \`pushed_feature_branch_needs_merge\` / \`blocked_review\` / \`cleanup_candidate\` / \`not_mergeable\`. Clean up with \`mesh_remove_node\`.
582
582
  - **Refinery is config-driven.** \`mesh_refine_node\` must run validation from \`.adhdev/refine.{json,yaml,yml}\` or \`repo-mesh.refine.*\`. Heuristics are scaffolding only.
583
583
  - **Submodule reachability = publish-needed.** \`submodule_reachability_failed\` → classify as \`blocked_review\`, request user approval to push to submodule main, then rerun \`mesh_refine_node\`.