@adhdev/daemon-core 0.9.82-rc.418 → 0.9.82-rc.419

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.
@@ -1,6 +1,6 @@
1
1
  import { type MeshRefineValidationCommandPlan, type RepoMeshRefineValidationCommandConfig } from './refine-config.js';
2
2
  import type { MeshAsyncJobLifecycle } from '../repo-mesh-types.js';
3
- export type WorktreeBootstrapStatus = 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
3
+ export type WorktreeBootstrapStatus = 'ready' | 'complete' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
4
4
  export interface RepoMeshWorktreeBootstrapConfig {
5
5
  version: 1;
6
6
  enabled?: boolean;
@@ -528,7 +528,7 @@ export interface LocalMeshNodeEntry {
528
528
  clonedFromNodeId?: string;
529
529
  /** Repo-local preparation result for ADHDev-created worktree nodes. */
530
530
  worktreeBootstrap?: {
531
- status: 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
531
+ status: 'ready' | 'complete' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
532
532
  required?: boolean;
533
533
  configSource?: string;
534
534
  configSourceType?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.418",
3
+ "version": "0.9.82-rc.419",
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,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.418",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.419",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -13,7 +13,12 @@ import {
13
13
  } from './refine-config.js';
14
14
  import type { MeshAsyncJobLifecycle } from '../repo-mesh-types.js';
15
15
 
16
- export type WorktreeBootstrapStatus = 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
16
+ // 'complete' is the terminal status the coordinator's markWorktreeBootstrapTerminalState
17
+ // (router.ts) stamps when the worktree_bootstrap_complete event fires — distinct from
18
+ // 'ready' (which carries a staleInputs digest from an in-process run). It is terminal and
19
+ // must be recognized as such by evaluateWorktreeBootstrapState so a later re-hydration never
20
+ // round-trips it back to 'stale'/'never_ran'.
21
+ export type WorktreeBootstrapStatus = 'ready' | 'complete' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
17
22
 
18
23
  export interface RepoMeshWorktreeBootstrapConfig {
19
24
  version: 1;
@@ -56,6 +61,65 @@ export interface WorktreeBootstrapState extends MeshAsyncJobLifecycle {
56
61
  // local workspace on disk, or git errors) it returns false and the gate stays closed.
57
62
  export const WORKTREE_BOOTSTRAP_STALE_RUNNING_MS = 10 * 60 * 1000;
58
63
 
64
+ /**
65
+ * Enumerate registered submodule paths for a worktree, generically (no hardcoded
66
+ * 'oss'/'adhdev-providers'). Reads `.gitmodules` via `git config --file .gitmodules
67
+ * --get-regexp path`, whose lines are `submodule.<name>.path <relativePath>`. Paths
68
+ * are returned normalized to forward slashes (git porcelain always emits '/'),
69
+ * trailing slash stripped. Returns an empty set when there are no submodules or the
70
+ * lookup fails — callers must then treat any change as dirty (conservative).
71
+ */
72
+ function getRegisteredSubmodulePaths(workspace: string): Set<string> {
73
+ const paths = new Set<string>();
74
+ try {
75
+ const out = execFileSync(
76
+ resolveWin32Executable('git'),
77
+ ['config', '--file', '.gitmodules', '--get-regexp', 'path'],
78
+ { cwd: workspace, encoding: 'utf8', timeout: 10_000, windowsHide: true },
79
+ );
80
+ for (const line of String(out).split(/\r?\n/)) {
81
+ const trimmed = line.trim();
82
+ if (!trimmed) continue;
83
+ // `submodule.<name>.path <relativePath>` — value is everything after the first space.
84
+ const spaceIdx = trimmed.indexOf(' ');
85
+ if (spaceIdx < 0) continue;
86
+ const value = trimmed.slice(spaceIdx + 1).trim().replace(/\\/g, '/').replace(/\/+$/, '');
87
+ if (value) paths.add(value);
88
+ }
89
+ } catch {
90
+ // No .gitmodules / not a submodule superproject / git error → no exemptions.
91
+ }
92
+ return paths;
93
+ }
94
+
95
+ /**
96
+ * True when `git status --porcelain` output represents a worktree that is clean
97
+ * EXCEPT for submodule-gitlink-pointer moves. A worktree task that commits inside a
98
+ * registered submodule (e.g. oss/) leaves the superproject's gitlink outOfSync — a
99
+ * porcelain line of exactly " M <submodulePath>" (or "M <submodulePath>") — which is
100
+ * a NORMAL product of that task and must NOT disqualify the stale backstop. Any other
101
+ * line (a real file edit, an untracked file, a staged add, an unmerged path) means the
102
+ * tree is genuinely dirty → not clean. We filter gitlink lines explicitly rather than
103
+ * using `git status --ignore-submodules=all`, which would also mask a submodule with
104
+ * genuinely-uncommitted *content* (=dirty still surfaces pointer moves we want to ignore;
105
+ * =all would over-suppress).
106
+ */
107
+ function isCleanIgnoringSubmoduleGitlinks(porcelain: string, submodulePaths: Set<string>): boolean {
108
+ const lines = porcelain.split(/\r?\n/).filter(line => line.length > 0);
109
+ for (const line of lines) {
110
+ // Porcelain v1 line: two status chars (XY) + space + path. A submodule gitlink
111
+ // pointer move shows X or Y as 'M' with the other position a space, e.g.
112
+ // " M oss" (worktree-modified) or "M oss" (index-modified). Quoted paths
113
+ // (core.quotePath) wrap in double-quotes — those never match a bare submodule path,
114
+ // so they correctly fall through as dirty.
115
+ const status = line.slice(0, 2);
116
+ const path = line.slice(3).trim().replace(/\\/g, '/').replace(/\/+$/, '');
117
+ const isGitlinkPointerMove = (status === ' M' || status === 'M ') && submodulePaths.has(path);
118
+ if (!isGitlinkPointerMove) return false; // any non-gitlink change → dirty
119
+ }
120
+ return true;
121
+ }
122
+
59
123
  export function isWorktreeBootstrapStaleRunning(
60
124
  node: { worktreeBootstrap?: { status?: string; startedAt?: string; updatedAt?: string; completedAt?: string }; workspace?: string } | undefined,
61
125
  nowMs: number = Date.now(),
@@ -76,7 +140,15 @@ export function isWorktreeBootstrapStaleRunning(
76
140
  timeout: 10_000,
77
141
  windowsHide: true,
78
142
  });
79
- return String(out).trim() === '';
143
+ const porcelain = String(out).replace(/\r?\n$/, '');
144
+ if (porcelain.trim() === '') return true; // truly clean
145
+ // A worktree task that committed inside a submodule leaves the superproject's
146
+ // submodule-gitlink pointer outOfSync (" M oss"); that is the normal aftermath of the
147
+ // task this backstop exists to unstick, not an in-progress bootstrap, so a tree dirty
148
+ // ONLY by gitlink pointer moves still counts as clean here.
149
+ const submodulePaths = getRegisteredSubmodulePaths(workspace);
150
+ if (submodulePaths.size === 0) return false; // no submodules → no exemption, dirty
151
+ return isCleanIgnoringSubmoduleGitlinks(porcelain, submodulePaths);
80
152
  } catch {
81
153
  return false; // cannot verify clean → stay conservative (gate remains closed)
82
154
  }
@@ -238,6 +310,11 @@ export function evaluateWorktreeBootstrapState(mesh: any, workspace: string, per
238
310
  }
239
311
  if (persisted?.status === 'running') return { ...persisted, required };
240
312
  if (persisted?.status === 'failed') return { ...persisted, required };
313
+ // 'complete' is the terminal stamp written by markWorktreeBootstrapTerminalState
314
+ // (router.ts) on the worktree_bootstrap_complete event. Pass it through unchanged so a
315
+ // later getMeshWithCache re-hydration never re-derives it back to 'stale'/'never_ran' —
316
+ // doing so would reopen the dispatch/claim gate against a node whose bootstrap is done.
317
+ if (persisted?.status === 'complete') return { ...persisted, required };
241
318
  if (persisted?.status === 'ready') {
242
319
  const staleInputs = loaded.config.staleInputs ?? persisted.staleInputs ?? [];
243
320
  if (staleInputs.length > 0 && persisted.staleInputsDigest) {
@@ -769,7 +769,9 @@ export interface LocalMeshNodeEntry {
769
769
  clonedFromNodeId?: string;
770
770
  /** Repo-local preparation result for ADHDev-created worktree nodes. */
771
771
  worktreeBootstrap?: {
772
- status: 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
772
+ // 'complete' is the terminal stamp written by markWorktreeBootstrapTerminalState
773
+ // (router.ts) on worktree_bootstrap_complete — kept in sync with WorktreeBootstrapStatus.
774
+ status: 'ready' | 'complete' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
773
775
  required?: boolean;
774
776
  configSource?: string;
775
777
  configSourceType?: string;