@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.8",
3
+ "version": "0.9.82-rc.9",
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",
@@ -3117,7 +3117,7 @@ export class DaemonCommandRouter {
3117
3117
  continue;
3118
3118
  }
3119
3119
  try {
3120
- const gitStatus = await getGitRepoStatus(node.workspace as string, { timeoutMs: 10_000 });
3120
+ const gitStatus = await getGitRepoStatus(node.workspace as string, { timeoutMs: 10_000, refreshUpstream: true });
3121
3121
  status.git = gitStatus;
3122
3122
  if (gitStatus.isGitRepo) {
3123
3123
  status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
@@ -62,7 +62,7 @@ export interface GitPushResult extends GitRepoIdentity {
62
62
  }
63
63
 
64
64
  export interface GitCommandServices {
65
- getStatus?: (params: { workspace: string }) => Promise<GitRepoStatus> | GitRepoStatus;
65
+ getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
66
66
  getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
67
67
  getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
68
68
  createSnapshot?: (params: {
@@ -171,7 +171,7 @@ const defaultSnapshotStore = createGitSnapshotStore({
171
171
 
172
172
  export function createDefaultGitCommandServices(): GitCommandServices {
173
173
  return {
174
- getStatus: ({ workspace }) => getGitRepoStatus(workspace),
174
+ getStatus: ({ workspace, refreshUpstream }) => getGitRepoStatus(workspace, { refreshUpstream }),
175
175
  getDiffSummary: ({ workspace }) => getGitDiffSummary(workspace),
176
176
  getDiffFile: ({ workspace, path: filePath }) => getGitFileDiff(workspace, filePath),
177
177
  createSnapshot: ({ workspace, reason, sessionId, turnId }) => defaultSnapshotStore.create({
@@ -290,7 +290,7 @@ export async function handleGitCommand(
290
290
  switch (command) {
291
291
  case 'git_status': {
292
292
  if (!services.getStatus) return serviceNotImplemented(command);
293
- const status = await runService(() => services.getStatus!({ workspace }));
293
+ const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
294
294
  return 'success' in status ? status : { success: true, status };
295
295
  }
296
296
 
@@ -1,12 +1,25 @@
1
- import type { GitRepoStatus, GitSubmoduleStatus } from './git-types.js';
1
+ import type { GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
2
2
  import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
3
3
 
4
+ type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
5
+
4
6
  export interface GitStatusOptions {
5
7
  timeoutMs?: number;
6
8
  /** When true, include submodule status in the result. Defaults to true. */
7
9
  includeSubmodules?: boolean;
8
10
  /** Optional filter to exclude specific submodule paths from status */
9
11
  submoduleIgnorePaths?: string[];
12
+ /**
13
+ * When true, refresh the tracked remote before trusting ahead/behind.
14
+ * Callers should opt into this only for convergence-critical surfaces.
15
+ */
16
+ refreshUpstream?: boolean;
17
+ }
18
+
19
+ interface GitUpstreamProbe {
20
+ upstreamStatus: GitUpstreamFreshness;
21
+ upstreamFetchedAt?: number;
22
+ upstreamFetchError?: string;
10
23
  }
11
24
 
12
25
  export async function getGitRepoStatus(
@@ -18,8 +31,16 @@ export async function getGitRepoStatus(
18
31
 
19
32
  try {
20
33
  const repo = await resolveGitRepository(workspace, options);
21
- const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
22
- const parsed = parsePorcelainV2Status(statusOutput.stdout);
34
+ let parsed = await readPorcelainStatus(repo, options);
35
+ let upstreamProbe: GitUpstreamProbe = getInitialUpstreamProbe(parsed);
36
+
37
+ if (options.refreshUpstream) {
38
+ upstreamProbe = await refreshTrackedUpstream(repo, parsed, options);
39
+ if (upstreamProbe.upstreamStatus === 'fresh') {
40
+ parsed = await readPorcelainStatus(repo, options);
41
+ }
42
+ }
43
+
23
44
  const head = await readHead(repo, options);
24
45
  const stashCount = await readStashCount(repo, options);
25
46
 
@@ -36,6 +57,9 @@ export async function getGitRepoStatus(
36
57
  headCommit: head.commit,
37
58
  headMessage: head.message,
38
59
  upstream: parsed.upstream,
60
+ upstreamStatus: parsed.upstream ? upstreamProbe.upstreamStatus : 'no_upstream',
61
+ upstreamFetchedAt: upstreamProbe.upstreamFetchedAt,
62
+ upstreamFetchError: upstreamProbe.upstreamFetchError,
39
63
  ahead: parsed.ahead,
40
64
  behind: parsed.behind,
41
65
  staged: parsed.staged,
@@ -74,6 +98,72 @@ interface ParsedPorcelainStatus {
74
98
  conflictFiles: string[];
75
99
  }
76
100
 
101
+ async function readPorcelainStatus(repo: ResolvedGitRepo, options: GitStatusOptions): Promise<ParsedPorcelainStatus> {
102
+ const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
103
+ return parsePorcelainV2Status(statusOutput.stdout);
104
+ }
105
+
106
+ function getInitialUpstreamProbe(parsed: ParsedPorcelainStatus): GitUpstreamProbe {
107
+ return {
108
+ upstreamStatus: parsed.upstream ? 'unchecked' : 'no_upstream',
109
+ };
110
+ }
111
+
112
+ async function refreshTrackedUpstream(
113
+ repo: ResolvedGitRepo,
114
+ parsed: ParsedPorcelainStatus,
115
+ options: GitStatusOptions,
116
+ ): Promise<GitUpstreamProbe> {
117
+ if (!parsed.upstream || !parsed.branch) {
118
+ return { upstreamStatus: 'no_upstream' };
119
+ }
120
+
121
+ const remoteName = (await readBranchRemote(repo, parsed.branch, options)) ?? inferRemoteName(parsed.upstream);
122
+ if (!remoteName) {
123
+ return {
124
+ upstreamStatus: 'stale',
125
+ upstreamFetchError: `Unable to resolve remote for upstream '${parsed.upstream}'`,
126
+ };
127
+ }
128
+
129
+ try {
130
+ await runGit(repo, ['fetch', '--quiet', '--prune', '--no-tags', remoteName], options);
131
+ return {
132
+ upstreamStatus: 'fresh',
133
+ upstreamFetchedAt: Date.now(),
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ upstreamStatus: 'stale',
138
+ upstreamFetchError: formatGitError(error),
139
+ };
140
+ }
141
+ }
142
+
143
+ async function readBranchRemote(repo: ResolvedGitRepo, branch: string, options: GitStatusOptions): Promise<string | null> {
144
+ try {
145
+ const result = await runGit(repo, ['config', '--get', `branch.${branch}.remote`], options);
146
+ return result.stdout.trim() || null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ function inferRemoteName(upstream: string): string | null {
153
+ const [remoteName] = upstream.split('/');
154
+ return remoteName?.trim() || null;
155
+ }
156
+
157
+ function formatGitError(error: unknown): string {
158
+ if (error instanceof GitCommandError) {
159
+ return error.stderr || error.message;
160
+ }
161
+ if (error instanceof Error) {
162
+ return error.message;
163
+ }
164
+ return String(error);
165
+ }
166
+
77
167
  export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
78
168
  const parsed: ParsedPorcelainStatus = {
79
169
  branch: null,
@@ -145,7 +235,7 @@ export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
145
235
  }
146
236
 
147
237
  async function readHead(
148
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
238
+ repo: ResolvedGitRepo,
149
239
  options: GitStatusOptions,
150
240
  ): Promise<{ commit: string | null; message: string | null }> {
151
241
  try {
@@ -163,7 +253,7 @@ async function readHead(
163
253
  }
164
254
 
165
255
  async function readStashCount(
166
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
256
+ repo: ResolvedGitRepo,
167
257
  options: GitStatusOptions,
168
258
  ): Promise<number> {
169
259
  try {
@@ -187,6 +277,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
187
277
  headCommit: null,
188
278
  headMessage: null,
189
279
  upstream: null,
280
+ upstreamStatus: 'unavailable',
190
281
  ahead: 0,
191
282
  behind: 0,
192
283
  staged: 0,
@@ -206,7 +297,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
206
297
  // ─── Submodule Status ───────────────────────────
207
298
 
208
299
  async function getSubmoduleStatuses(
209
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
300
+ repo: ResolvedGitRepo,
210
301
  options: GitStatusOptions,
211
302
  ): Promise<GitSubmoduleStatus[]> {
212
303
  if (!repo.repoRoot) return [];
@@ -22,6 +22,9 @@ export function createGitCompactSummary(status: GitRepoStatus, diffSummary?: Git
22
22
  isGitRepo: status.isGitRepo,
23
23
  repoRoot: status.repoRoot,
24
24
  branch: status.branch,
25
+ upstreamStatus: status.upstreamStatus,
26
+ upstreamFetchedAt: status.upstreamFetchedAt,
27
+ upstreamFetchError: status.upstreamFetchError,
25
28
  dirty:
26
29
  status.staged > 0 ||
27
30
  status.modified > 0 ||
@@ -40,11 +40,19 @@ export interface GitSubmoduleStatus {
40
40
  error?: string;
41
41
  }
42
42
 
43
+ export type GitUpstreamFreshness = 'fresh' | 'unchecked' | 'stale' | 'no_upstream' | 'unavailable';
44
+
43
45
  export interface GitRepoStatus extends GitRepoIdentity {
44
46
  branch: string | null;
45
47
  headCommit: string | null;
46
48
  headMessage: string | null;
47
49
  upstream: string | null;
50
+ /** Whether ahead/behind was verified against a freshly fetched upstream ref. */
51
+ upstreamStatus: GitUpstreamFreshness;
52
+ /** Timestamp for the fetch that refreshed upstream refs when upstreamStatus === 'fresh'. */
53
+ upstreamFetchedAt?: number;
54
+ /** Error from the last refresh attempt when upstreamStatus === 'stale'. */
55
+ upstreamFetchError?: string;
48
56
  ahead: number;
49
57
  behind: number;
50
58
  staged: number;
@@ -134,6 +142,9 @@ export interface GitCompactSummary {
134
142
  isGitRepo: boolean;
135
143
  repoRoot: string | null;
136
144
  branch: string | null;
145
+ upstreamStatus: GitUpstreamFreshness;
146
+ upstreamFetchedAt?: number;
147
+ upstreamFetchError?: string;
137
148
  dirty: boolean;
138
149
  changedFiles: number;
139
150
  ahead: number;