@phnx-labs/agents-cli 1.22.117 → 1.22.119

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 (271) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/accounts.js +7 -4
  4. package/dist/commands/browser.d.ts +52 -70
  5. package/dist/commands/browser.js +192 -3562
  6. package/dist/commands/daemon.d.ts +39 -2
  7. package/dist/commands/daemon.js +72 -73
  8. package/dist/commands/events.js +1 -1
  9. package/dist/commands/exec.js +53 -1
  10. package/dist/commands/projects.js +92 -7
  11. package/dist/commands/repo.js +13 -49
  12. package/dist/commands/route.js +5 -5
  13. package/dist/commands/routines.js +39 -12
  14. package/dist/commands/sessions-render.d.ts +1 -1
  15. package/dist/commands/sessions-render.js +0 -1
  16. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  17. package/dist/commands/sessions.test-fixture.js +0 -37
  18. package/dist/commands/setup-browser.d.ts +17 -12
  19. package/dist/commands/setup-browser.js +52 -87
  20. package/dist/commands/setup-preferences.d.ts +0 -17
  21. package/dist/commands/setup-preferences.js +18 -49
  22. package/dist/commands/setup.js +9 -9
  23. package/dist/commands/ssh.js +10 -1
  24. package/dist/commands/sync.js +6 -44
  25. package/dist/commands/teams.js +0 -1
  26. package/dist/commands/update.d.ts +19 -0
  27. package/dist/commands/update.js +34 -6
  28. package/dist/commands/view.d.ts +12 -1
  29. package/dist/commands/view.js +47 -2
  30. package/dist/index.js +107 -26
  31. package/dist/lib/account-capabilities.js +0 -2
  32. package/dist/lib/account-catalog.d.ts +38 -0
  33. package/dist/lib/account-catalog.js +110 -26
  34. package/dist/lib/account-provider-registry.js +1 -1
  35. package/dist/lib/account-registry.d.ts +1 -1
  36. package/dist/lib/account-registry.js +36 -2
  37. package/dist/lib/accounting/rotate.js +22 -8
  38. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  39. package/dist/lib/accounting/usage-ingest.js +115 -45
  40. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  41. package/dist/lib/accounting/usage-sync.js +227 -66
  42. package/dist/lib/accounting/usage.d.ts +16 -3
  43. package/dist/lib/accounting/usage.js +26 -8
  44. package/dist/lib/accounts/add.d.ts +3 -1
  45. package/dist/lib/accounts/add.js +24 -13
  46. package/dist/lib/accounts/slots.d.ts +8 -0
  47. package/dist/lib/accounts/slots.js +17 -0
  48. package/dist/lib/add-dir.js +0 -1
  49. package/dist/lib/agent-cli-commands.js +0 -1
  50. package/dist/lib/agent-spec/agents.d.ts +6 -7
  51. package/dist/lib/agent-spec/agents.js +7 -60
  52. package/dist/lib/auth-health.d.ts +69 -1
  53. package/dist/lib/auth-health.js +133 -4
  54. package/dist/lib/browser/context.d.ts +82 -0
  55. package/dist/lib/browser/context.js +88 -0
  56. package/dist/lib/browser/paths.d.ts +25 -0
  57. package/dist/lib/browser/paths.js +51 -0
  58. package/dist/lib/browser/record.d.ts +38 -0
  59. package/dist/lib/browser/record.js +57 -0
  60. package/dist/lib/browser/sessions-list.js +1 -2
  61. package/dist/lib/browser-client.d.ts +158 -0
  62. package/dist/lib/browser-client.js +234 -0
  63. package/dist/lib/channels/owner-sink.d.ts +1 -18
  64. package/dist/lib/channels/owner-sink.js +29 -82
  65. package/dist/lib/channels/providers/rush.d.ts +7 -7
  66. package/dist/lib/channels/providers/rush.js +127 -57
  67. package/dist/lib/claude-account-token.d.ts +7 -1
  68. package/dist/lib/claude-account-token.js +26 -4
  69. package/dist/lib/claude-statusline.d.ts +1 -1
  70. package/dist/lib/claude-statusline.js +15 -3
  71. package/dist/lib/commands.js +1 -1
  72. package/dist/lib/crabbox/lease.js +1 -1
  73. package/dist/lib/crabbox/runtimes.js +1 -3
  74. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  75. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  76. package/dist/lib/daemon/auth-sync-service.js +37 -43
  77. package/dist/lib/daemon/daemon.d.ts +25 -14
  78. package/dist/lib/daemon/daemon.js +92 -154
  79. package/dist/lib/daemon/runner.d.ts +2 -2
  80. package/dist/lib/daemon/runner.js +32 -9
  81. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  82. package/dist/lib/daemon/self-update-service.js +40 -36
  83. package/dist/lib/daemon/service.d.ts +12 -8
  84. package/dist/lib/daemon/supervisor.d.ts +65 -54
  85. package/dist/lib/daemon/supervisor.js +136 -171
  86. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  87. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  88. package/dist/lib/daemon/usage-sync-service.js +46 -78
  89. package/dist/lib/daemon-health.d.ts +22 -5
  90. package/dist/lib/daemon-health.js +47 -3
  91. package/dist/lib/daemon-services.d.ts +1 -1
  92. package/dist/lib/daemon-services.js +0 -10
  93. package/dist/lib/daemon-ticks.js +4 -2
  94. package/dist/lib/devices/doctor-findings.js +8 -11
  95. package/dist/lib/devices/harness-inventory.js +14 -5
  96. package/dist/lib/exec-account-home.js +5 -0
  97. package/dist/lib/exec.js +10 -13
  98. package/dist/lib/feed/events.js +0 -1
  99. package/dist/lib/feed/tool-activity.js +5 -2
  100. package/dist/lib/fleet-shared-state.d.ts +45 -0
  101. package/dist/lib/fleet-shared-state.js +78 -9
  102. package/dist/lib/fs-atomic.js +25 -0
  103. package/dist/lib/github/pr-mergeable.d.ts +4 -2
  104. package/dist/lib/github/pr-mergeable.js +5 -3
  105. package/dist/lib/github/project-prs.d.ts +58 -3
  106. package/dist/lib/github/project-prs.js +205 -7
  107. package/dist/lib/harness/adapters/cursor.js +14 -4
  108. package/dist/lib/harness-auth-capabilities.d.ts +4 -3
  109. package/dist/lib/harness-auth-capabilities.js +1 -2
  110. package/dist/lib/helper-versions.js +1 -1
  111. package/dist/lib/hooks/cache.d.ts +7 -0
  112. package/dist/lib/hooks/cache.js +27 -2
  113. package/dist/lib/hooks/install.js +7 -1
  114. package/dist/lib/hosts/ready.d.ts +38 -8
  115. package/dist/lib/hosts/ready.js +62 -14
  116. package/dist/lib/installations/active-check.d.ts +27 -0
  117. package/dist/lib/installations/active-check.js +61 -0
  118. package/dist/lib/installations/migrate.js +6 -3
  119. package/dist/lib/installations/shims.d.ts +1 -1
  120. package/dist/lib/installations/shims.js +32 -12
  121. package/dist/lib/installations/store.d.ts +9 -0
  122. package/dist/lib/installations/store.js +24 -0
  123. package/dist/lib/installations/update.js +21 -15
  124. package/dist/lib/installations/versions.js +15 -3
  125. package/dist/lib/manifest.js +1 -1
  126. package/dist/lib/models.js +0 -2
  127. package/dist/lib/open-url.js +28 -54
  128. package/dist/lib/profiles.js +0 -2
  129. package/dist/lib/projects.d.ts +16 -0
  130. package/dist/lib/projects.js +31 -0
  131. package/dist/lib/refresh.js +1 -1
  132. package/dist/lib/routine-readiness.d.ts +35 -0
  133. package/dist/lib/routine-readiness.js +74 -16
  134. package/dist/lib/rules/compile.js +6 -0
  135. package/dist/lib/sandbox.js +0 -2
  136. package/dist/lib/secrets-policy.d.ts +89 -11
  137. package/dist/lib/secrets-policy.js +175 -28
  138. package/dist/lib/session/active.d.ts +17 -6
  139. package/dist/lib/session/active.js +105 -49
  140. package/dist/lib/session/db.d.ts +1 -1
  141. package/dist/lib/session/db.js +65 -18
  142. package/dist/lib/session/discover.js +1 -283
  143. package/dist/lib/session/mirror.d.ts +4 -3
  144. package/dist/lib/session/mirror.js +4 -3
  145. package/dist/lib/session/parse.d.ts +0 -2
  146. package/dist/lib/session/parse.js +1 -138
  147. package/dist/lib/session/pid-registry.d.ts +6 -3
  148. package/dist/lib/session/pid-registry.js +18 -8
  149. package/dist/lib/session/remote/watch.d.ts +1 -1
  150. package/dist/lib/session/remote/watch.js +7 -3
  151. package/dist/lib/session/throughput.d.ts +1 -3
  152. package/dist/lib/session/throughput.js +0 -20
  153. package/dist/lib/session/types.d.ts +2 -2
  154. package/dist/lib/session/types.js +2 -2
  155. package/dist/lib/sessions-client.d.ts +60 -1
  156. package/dist/lib/sessions-client.js +227 -4
  157. package/dist/lib/setup-tool-status.js +5 -10
  158. package/dist/lib/signin-badge.d.ts +3 -2
  159. package/dist/lib/signin-badge.js +6 -3
  160. package/dist/lib/smart-launch.js +4 -1
  161. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  162. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  163. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  164. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  165. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  166. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  167. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  168. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  169. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  170. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  171. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  172. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  173. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  174. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  175. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  176. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  177. package/dist/lib/staleness/writers/sources.js +5 -0
  178. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  179. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  180. package/dist/lib/state.d.ts +6 -4
  181. package/dist/lib/state.js +7 -17
  182. package/dist/lib/teams/parsers.d.ts +1 -1
  183. package/dist/lib/teams/parsers.js +1 -136
  184. package/dist/lib/teams/placement-probe.js +50 -15
  185. package/dist/lib/teams/scheduler.d.ts +7 -0
  186. package/dist/lib/types.d.ts +16 -1
  187. package/dist/lib/types.js +1 -1
  188. package/dist/lib/view-types.d.ts +32 -0
  189. package/dist/lib/watchdog/read.js +0 -1
  190. package/dist/lib/watchdog/rotate.js +1 -2
  191. package/dist/lib/watchdog/runner.js +2 -2
  192. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  193. package/dist/lib/watchdog/watchdogTail.js +0 -31
  194. package/package.json +1 -1
  195. package/scripts/postinstall.js +8 -8
  196. package/dist/browser.d.ts +0 -2
  197. package/dist/browser.js +0 -17
  198. package/dist/commands/browser-picker.d.ts +0 -18
  199. package/dist/commands/browser-picker.js +0 -97
  200. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  201. package/dist/lib/browser/arc-discovery.js +0 -197
  202. package/dist/lib/browser/arc-dom.d.ts +0 -14
  203. package/dist/lib/browser/arc-dom.js +0 -121
  204. package/dist/lib/browser/caller-identity.d.ts +0 -40
  205. package/dist/lib/browser/caller-identity.js +0 -175
  206. package/dist/lib/browser/cdp.d.ts +0 -51
  207. package/dist/lib/browser/cdp.js +0 -254
  208. package/dist/lib/browser/chrome.d.ts +0 -141
  209. package/dist/lib/browser/chrome.js +0 -704
  210. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  211. package/dist/lib/browser/chromium-discovery.js +0 -96
  212. package/dist/lib/browser/devices.d.ts +0 -34
  213. package/dist/lib/browser/devices.js +0 -61
  214. package/dist/lib/browser/domain-skills.d.ts +0 -71
  215. package/dist/lib/browser/domain-skills.js +0 -195
  216. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  217. package/dist/lib/browser/drivers/arc.js +0 -276
  218. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  219. package/dist/lib/browser/drivers/firefox.js +0 -377
  220. package/dist/lib/browser/drivers/local.d.ts +0 -55
  221. package/dist/lib/browser/drivers/local.js +0 -266
  222. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  223. package/dist/lib/browser/drivers/ssh.js +0 -467
  224. package/dist/lib/browser/editor.d.ts +0 -3
  225. package/dist/lib/browser/editor.js +0 -50
  226. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  227. package/dist/lib/browser/ffmpeg.js +0 -184
  228. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  229. package/dist/lib/browser/firefox-discovery.js +0 -162
  230. package/dist/lib/browser/har.d.ts +0 -85
  231. package/dist/lib/browser/har.js +0 -77
  232. package/dist/lib/browser/hygiene.d.ts +0 -97
  233. package/dist/lib/browser/hygiene.js +0 -153
  234. package/dist/lib/browser/index.d.ts +0 -5
  235. package/dist/lib/browser/index.js +0 -5
  236. package/dist/lib/browser/input.d.ts +0 -7
  237. package/dist/lib/browser/input.js +0 -92
  238. package/dist/lib/browser/ipc.d.ts +0 -204
  239. package/dist/lib/browser/ipc.js +0 -1453
  240. package/dist/lib/browser/login-detection.d.ts +0 -87
  241. package/dist/lib/browser/login-detection.js +0 -274
  242. package/dist/lib/browser/profiles.d.ts +0 -422
  243. package/dist/lib/browser/profiles.js +0 -1157
  244. package/dist/lib/browser/refs.d.ts +0 -89
  245. package/dist/lib/browser/refs.js +0 -191
  246. package/dist/lib/browser/registry.d.ts +0 -70
  247. package/dist/lib/browser/registry.js +0 -252
  248. package/dist/lib/browser/remote-control.d.ts +0 -59
  249. package/dist/lib/browser/remote-control.js +0 -85
  250. package/dist/lib/browser/resolve-target.d.ts +0 -66
  251. package/dist/lib/browser/resolve-target.js +0 -264
  252. package/dist/lib/browser/runtime-state.d.ts +0 -291
  253. package/dist/lib/browser/runtime-state.js +0 -584
  254. package/dist/lib/browser/secret-ref.d.ts +0 -10
  255. package/dist/lib/browser/secret-ref.js +0 -14
  256. package/dist/lib/browser/service.d.ts +0 -812
  257. package/dist/lib/browser/service.js +0 -4693
  258. package/dist/lib/browser/stream.d.ts +0 -17
  259. package/dist/lib/browser/stream.js +0 -72
  260. package/dist/lib/browser/task-index.d.ts +0 -78
  261. package/dist/lib/browser/task-index.js +0 -199
  262. package/dist/lib/browser/types.d.ts +0 -647
  263. package/dist/lib/browser/types.js +0 -61
  264. package/dist/lib/browser/upload.d.ts +0 -29
  265. package/dist/lib/browser/upload.js +0 -298
  266. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  267. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  268. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  269. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  270. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  271. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -1,490 +0,0 @@
1
- /**
2
- * Automatic transport for the non-secret fleet state stored in the user repo.
3
- *
4
- * Usage/auth services publish only their own conflict-free
5
- * `devices/<device>/daemon-state.json` file. This module commits that one path,
6
- * rebases the user repo, and pushes it so peer daemons can read the snapshot
7
- * after their own fetch/rebase. Git runs asynchronously under one cross-process
8
- * lock; every invocation has a wall-clock deadline and a timed-out process tree
9
- * is terminated, including git's ssh child and its remote channel.
10
- */
11
- import { spawn } from 'node:child_process';
12
- import * as fs from 'node:fs';
13
- import * as path from 'node:path';
14
- import lockfile from 'proper-lockfile';
15
- import { fleetSharedStatePath } from './fleet-shared-state.js';
16
- import { ensureLockTarget } from './fs-atomic.js';
17
- import { logAndContinueOnLockCompromised } from './lock-compromise.js';
18
- import { machineId } from './session/sync/config.js';
19
- import { getDaemonDir, getUserAgentsDir } from './state.js';
20
- export const FLEET_SHARED_REPO_SYNC_DEADLINE_MS = 45_000;
21
- const FLEET_SHARED_REPO_KILL_GRACE_MS = 250;
22
- export const FLEET_SHARED_REPO_OUTPUT_MAX_BYTES = 1024 * 1024;
23
- const FLEET_SHARED_REPO_PUSH_ATTEMPTS = 3;
24
- const FLEET_SHARED_REPO_REBASE_CLEANUP_RESERVE_MS = 5_000;
25
- /**
26
- * The exchange records its last SUCCESSFUL completion beside its lock so a
27
- * sibling service (auth-sync) can gate work on the freshness of the peer state
28
- * this exchange delivers into the local checkout (PHNX-4051). auth-sync's
29
- * credential pushes read the peer verdicts the LAST usage-sync exchange wrote; if
30
- * that exchange has not landed within a tick interval, those verdicts are stale
31
- * and pushing off them would act on a peer that may already hold the key.
32
- */
33
- function exchangeMarkerPath(lockPath) {
34
- return `${lockPath}.last-success`;
35
- }
36
- function writeExchangeMarker(lockPath, nowMs = Date.now()) {
37
- try {
38
- const p = exchangeMarkerPath(lockPath);
39
- fs.mkdirSync(path.dirname(p), { recursive: true });
40
- fs.writeFileSync(p, JSON.stringify({ at: nowMs }), 'utf-8');
41
- }
42
- catch {
43
- // Best-effort telemetry — never fail the exchange over its own marker.
44
- }
45
- }
46
- /**
47
- * When the shared-state exchange last completed successfully, in epoch ms, or
48
- * `null` if it never has on this box (no marker yet) or the marker is unreadable.
49
- * Reads the default daemon-dir marker the periodic exchange writes.
50
- */
51
- export function readLastSuccessfulExchangeMs() {
52
- try {
53
- const lockPath = path.join(getDaemonDir(), 'fleet-shared-repo-sync');
54
- const raw = fs.readFileSync(exchangeMarkerPath(lockPath), 'utf-8');
55
- const at = JSON.parse(raw).at;
56
- return typeof at === 'number' && Number.isFinite(at) ? at : null;
57
- }
58
- catch {
59
- return null;
60
- }
61
- }
62
- function signalProcessTree(child, signal) {
63
- const pid = child.pid;
64
- if (!pid)
65
- return;
66
- if (process.platform === 'win32') {
67
- // Windows has no process-group signal. taskkill is asynchronous here so a
68
- // timeout path cannot replace one event-loop stall with another.
69
- const killer = spawn('taskkill', ['/F', '/T', '/PID', String(pid)], {
70
- stdio: 'ignore',
71
- windowsHide: true,
72
- });
73
- killer.unref();
74
- return;
75
- }
76
- try {
77
- // The child is spawned detached on POSIX, so -pid reaches git plus ssh.
78
- process.kill(-pid, signal);
79
- }
80
- catch {
81
- try {
82
- child.kill(signal);
83
- }
84
- catch { /* already gone */ }
85
- }
86
- }
87
- /** Run one real process asynchronously with a hard wall-clock/process-tree bound. */
88
- export function runBoundedProcess(command, args, options) {
89
- return new Promise((resolve) => {
90
- const child = spawn(command, args, {
91
- cwd: options.cwd,
92
- env: options.env ?? process.env,
93
- detached: process.platform !== 'win32',
94
- stdio: ['ignore', 'pipe', 'pipe'],
95
- windowsHide: true,
96
- });
97
- let stdout = '';
98
- let stderr = '';
99
- let settled = false;
100
- let timedOut = false;
101
- let overflow = false;
102
- let killTimer = null;
103
- const finish = (code) => {
104
- if (settled)
105
- return;
106
- settled = true;
107
- clearTimeout(timer);
108
- if (killTimer)
109
- clearTimeout(killTimer);
110
- resolve({ code, stdout, stderr, timedOut });
111
- };
112
- const hardKill = () => {
113
- signalProcessTree(child, 'SIGKILL');
114
- if (process.platform === 'win32' && !killTimer) {
115
- // taskkill owns the descendant cleanup; this fallback only guarantees
116
- // the direct child's Promise settles if taskkill itself cannot run.
117
- killTimer = setTimeout(() => {
118
- if (!settled) {
119
- try {
120
- child.kill('SIGKILL');
121
- }
122
- catch { /* already gone */ }
123
- }
124
- }, FLEET_SHARED_REPO_KILL_GRACE_MS);
125
- }
126
- };
127
- const timer = setTimeout(() => {
128
- timedOut = true;
129
- if (process.platform === 'win32') {
130
- hardKill();
131
- return;
132
- }
133
- signalProcessTree(child, 'SIGTERM');
134
- killTimer = setTimeout(() => {
135
- if (!settled)
136
- hardKill();
137
- }, FLEET_SHARED_REPO_KILL_GRACE_MS);
138
- }, Math.max(1, options.timeoutMs));
139
- const append = (stream, chunk) => {
140
- if (overflow)
141
- return;
142
- const value = String(chunk);
143
- if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) + Buffer.byteLength(value) > FLEET_SHARED_REPO_OUTPUT_MAX_BYTES) {
144
- overflow = true;
145
- stderr += '\ngit output exceeded 1 MiB';
146
- hardKill();
147
- return;
148
- }
149
- if (stream === 'stdout')
150
- stdout += value;
151
- else
152
- stderr += value;
153
- };
154
- child.stdout?.on('data', (chunk) => append('stdout', chunk));
155
- child.stderr?.on('data', (chunk) => append('stderr', chunk));
156
- child.on('error', (err) => {
157
- stderr += err.message;
158
- finish(null);
159
- });
160
- child.on('close', (code) => finish(code));
161
- });
162
- }
163
- function failure(command, result) {
164
- const detail = result.timedOut
165
- ? `${command} timed out`
166
- : result.stderr.trim() || result.stdout.trim() || `${command} exited ${result.code ?? 'without a status'}`;
167
- return {
168
- success: false,
169
- committed: false,
170
- pushed: false,
171
- commit: null,
172
- timedOut: result.timedOut,
173
- skipped: null,
174
- error: detail,
175
- };
176
- }
177
- function isPushRace(result) {
178
- const text = `${result.stdout}\n${result.stderr}`.toLowerCase();
179
- return text.includes('non-fast-forward') || text.includes('fetch first') || text.includes('[rejected]');
180
- }
181
- function createdAutostashObject(result) {
182
- return `${result.stdout}\n${result.stderr}`.match(/created autostash:\s*([0-9a-f]+)/i)?.[1] ?? null;
183
- }
184
- function retainedAutostash(stashList, objectId) {
185
- if (stashList.code === 0) {
186
- const entries = stashList.stdout.trim().split('\n').filter(Boolean).map((line) => {
187
- const [ref = '', hash = '', ...subject] = line.split('\t');
188
- return { ref, hash, subject: subject.join('\t') };
189
- });
190
- const matched = objectId
191
- ? entries.find((entry) => entry.hash.startsWith(objectId) || objectId.startsWith(entry.hash))
192
- : entries.find((entry) => /autostash/i.test(entry.subject));
193
- if (matched?.ref)
194
- return { label: `${matched.ref} (${matched.hash.slice(0, 8)})`, ref: matched.ref };
195
- }
196
- return {
197
- label: objectId ? `autostash object ${objectId}` : 'the retained autostash',
198
- ref: null,
199
- };
200
- }
201
- /**
202
- * `git rebase` checks out its base (origin/<branch>) and aborts when an
203
- * untracked working-tree file would be overwritten by a file that origin
204
- * tracks. `--autostash` only sets aside *tracked* changes, so these untracked
205
- * collisions wedge the rebase on every sync and the device silently falls
206
- * hundreds of commits behind — the fleet-wide drift root cause (PHNX-3923).
207
- *
208
- * In this shared-state repo an untracked file that also exists on origin is a
209
- * stale local snapshot: the canonical copy is on origin and every device
210
- * republishes its own state (this device's own owned file is already committed
211
- * before we get here, so it is tracked and never appears below). Drop the ones
212
- * byte-identical to origin (lossless) and move any that differ into a backup
213
- * dir beside the repo root — outside the repo's tracked tree, so the move
214
- * cannot create a fresh collision — so nothing is silently destroyed. Then let
215
- * the rebase check out origin's version. A file we cannot clear is left in
216
- * place; the rebase may still abort, no worse than today and never losing data.
217
- *
218
- * The scan is scoped to the TRACKED ROOTS origin carries — the top-level entries
219
- * of `git ls-tree --name-only origin/<branch>` — NOT the whole working tree
220
- * (PHNX-4051). Only a file origin tracks can be overwritten by the rebase
221
- * checkout, so a tracked root is the tightest superset of the collidable paths:
222
- * every path origin tracks stays in scope (a stale untracked `projects/rush.yaml`
223
- * under a tracked `projects/` root is still cleared), while an untracked-ONLY tree
224
- * origin never tracks — artifacts-cli's revision store
225
- * (`~/.agents/artifact-history/`, ~11k files / >1 MiB of paths) is the live case —
226
- * is never a tracked root, so it never enters the `git ls-files --others` listing.
227
- * An unbounded listing there blows past {@link FLEET_SHARED_REPO_OUTPUT_MAX_BYTES}
228
- * and kills the exchange on every tick; scoping to the tracked roots keeps the
229
- * PHNX-3923 coverage whole while an unrelated untracked tree cannot push the
230
- * listing over the cap. `ls-tree`'s own output is bounded — it lists only the root
231
- * entries, never the recursive tree.
232
- */
233
- async function clearCollidingUntracked(git, root, branch) {
234
- const tree = await git(['ls-tree', '--name-only', '-z', `origin/${branch}`]);
235
- if (tree.code !== 0)
236
- return { error: tree };
237
- const trackedRoots = tree.stdout.split('\0').filter(Boolean);
238
- if (trackedRoots.length === 0)
239
- return { cleared: 0, backedUp: [], backupDir: null };
240
- const others = await git(['ls-files', '--others', '--exclude-standard', '-z', '--', ...trackedRoots]);
241
- if (others.code !== 0)
242
- return { error: others };
243
- const relPaths = others.stdout.split('\0').filter(Boolean);
244
- let cleared = 0;
245
- const backedUp = [];
246
- let backupDir = null;
247
- for (const rel of relPaths) {
248
- // rev-parse doubles as the "does origin track this path" test (non-zero =
249
- // absent) — only paths origin tracks are overwritten by the rebase checkout.
250
- const originHash = await git(['rev-parse', `origin/${branch}:${rel}`]);
251
- if (originHash.code !== 0 || !originHash.stdout.trim())
252
- continue;
253
- const abs = path.join(root, rel);
254
- // Byte-exact identity via git blob SHAs. Comparing UTF-8-decoded strings
255
- // would collapse distinct invalid bytes to U+FFFD and could delete
256
- // non-identical content. `--no-filters` hashes the raw on-disk bytes (no
257
- // clean/autocrlf normalization), so `identical` is true only when the local
258
- // bytes exactly equal origin's stored blob — never a filter-normalized
259
- // near-match. A hash-object failure (unreadable / vanished under a
260
- // concurrent writer) means "leave it in place".
261
- const localHash = await git(['hash-object', '--no-filters', '--', abs]);
262
- if (localHash.code !== 0 || !localHash.stdout.trim())
263
- continue;
264
- const identical = originHash.stdout.trim() === localHash.stdout.trim();
265
- try {
266
- if (identical) {
267
- fs.rmSync(abs, { force: true });
268
- cleared++;
269
- }
270
- else {
271
- if (!backupDir) {
272
- // A sibling of the repo root: outside the tracked tree (so the move
273
- // cannot create a fresh collision) yet colocated for easy recovery.
274
- backupDir = path.join(`${root}-fleet-sync-backups`, String(Date.now()));
275
- }
276
- const dest = path.join(backupDir, rel);
277
- fs.mkdirSync(path.dirname(dest), { recursive: true });
278
- fs.renameSync(abs, dest);
279
- backedUp.push(rel);
280
- }
281
- }
282
- catch {
283
- // Could not clear this collision; leave it in place.
284
- }
285
- }
286
- return { cleared, backedUp, backupDir };
287
- }
288
- async function performFleetSharedRepoSync(root, device, timeoutMs) {
289
- if (!fs.existsSync(path.join(root, '.git'))) {
290
- return {
291
- success: false,
292
- committed: false,
293
- pushed: false,
294
- commit: null,
295
- timedOut: false,
296
- skipped: `user store is not git-backed: ${root}`,
297
- error: null,
298
- };
299
- }
300
- const deadline = Date.now() + timeoutMs;
301
- const git = async (args, reserveMs = 0) => {
302
- const remaining = deadline - Date.now() - reserveMs;
303
- if (remaining <= 0)
304
- return { code: null, stdout: '', stderr: '', timedOut: true };
305
- return runBoundedProcess('git', args, {
306
- cwd: root,
307
- timeoutMs: remaining,
308
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
309
- });
310
- };
311
- const branchResult = await git(['branch', '--show-current']);
312
- if (branchResult.code !== 0)
313
- return failure('git branch --show-current', branchResult);
314
- const branch = branchResult.stdout.trim();
315
- if (!branch) {
316
- return {
317
- success: false,
318
- committed: false,
319
- pushed: false,
320
- commit: null,
321
- timedOut: false,
322
- skipped: null,
323
- error: 'user store is on a detached HEAD',
324
- };
325
- }
326
- const ownedFile = fleetSharedStatePath(device, root);
327
- const relativeOwnedFile = path.relative(root, ownedFile).split(path.sep).join('/');
328
- // The central `agents.yaml` is fleet-shared state too — native account rows
329
- // land there (lib/state.ts writeMetaUnlocked) as a plain file write with no
330
- // commit. Publishing only the per-device file and then `rebase --autostash`-ing
331
- // over a dirty central file silently destroyed those names: every box lost its
332
- // account names on its next daemon publish (PHNX-3887). Commit it alongside the
333
- // device doc so the rebase carries the rows instead of stashing them.
334
- const centralFile = path.join(root, 'agents.yaml');
335
- const publishPaths = [relativeOwnedFile];
336
- if (fs.existsSync(centralFile))
337
- publishPaths.push('agents.yaml');
338
- const existingPaths = publishPaths.filter(rel => fs.existsSync(path.join(root, rel)));
339
- let committed = false;
340
- if (existingPaths.length > 0) {
341
- const status = await git(['status', '--porcelain=v1', '--', ...existingPaths]);
342
- if (status.code !== 0)
343
- return failure('git status', status);
344
- if (status.stdout.trim()) {
345
- const add = await git(['add', '--', ...existingPaths]);
346
- if (add.code !== 0)
347
- return failure('git add', add);
348
- const commit = await git([
349
- '-c', 'commit.gpgsign=false',
350
- 'commit', '--no-verify', '-m', `chore(devices): publish ${device} daemon state`,
351
- '--', ...existingPaths,
352
- ]);
353
- if (commit.code !== 0)
354
- return failure('git commit', commit);
355
- committed = true;
356
- }
357
- }
358
- let untrackedCleared = 0;
359
- const untrackedBackedUp = [];
360
- let untrackedBackupDir;
361
- for (let attempt = 1; attempt <= FLEET_SHARED_REPO_PUSH_ATTEMPTS; attempt++) {
362
- const fetch = await git(['fetch', 'origin']);
363
- if (fetch.code !== 0)
364
- return { ...failure('git fetch', fetch), committed };
365
- // Untracked files that origin tracks would abort the rebase's checkout
366
- // (autostash only covers tracked changes). Clear them first — this is the
367
- // fleet-drift root cause (PHNX-3923).
368
- const reconcile = await clearCollidingUntracked(git, root, branch);
369
- if ('error' in reconcile) {
370
- return { ...failure('git ls-files --others', reconcile.error), committed };
371
- }
372
- untrackedCleared += reconcile.cleared;
373
- untrackedBackedUp.push(...reconcile.backedUp);
374
- if (reconcile.backupDir)
375
- untrackedBackupDir = reconcile.backupDir;
376
- // Keep enough of the same wall-clock bound available to remove a botched
377
- // autostash pop from the operator's live checkout before returning.
378
- const rebase = await git(['rebase', '--autostash', `origin/${branch}`], FLEET_SHARED_REPO_REBASE_CLEANUP_RESERVE_MS);
379
- if (rebase.code !== 0) {
380
- // Rebase can leave control files behind even when the child was killed.
381
- // Abort is bounded by the same overall deadline and never hides the cause.
382
- await git(['rebase', '--abort']);
383
- return { ...failure('git rebase', rebase), committed };
384
- }
385
- // `git rebase --autostash` exits zero when the rebase succeeds but applying
386
- // the autostash conflicts. Never push or leave those conflict markers in
387
- // the operator's real ~/.agents checkout. Git retains the original changes
388
- // in the stash on this path, so reset only the failed pop to rebased HEAD.
389
- const unmerged = await git(['ls-files', '--unmerged']);
390
- if (unmerged.code !== 0)
391
- return { ...failure('git ls-files --unmerged', unmerged), committed };
392
- if (unmerged.stdout.trim()) {
393
- const autostashObject = createdAutostashObject(rebase);
394
- const reset = await git(['reset', '--hard', 'HEAD']);
395
- const cleaned = await git(['ls-files', '--unmerged']);
396
- const stashList = await git(['stash', 'list', '--format=%gd%x09%H%x09%gs']);
397
- const stash = retainedAutostash(stashList, autostashObject);
398
- if (reset.code !== 0 || cleaned.code !== 0 || cleaned.stdout.trim()) {
399
- const cleanup = reset.code !== 0
400
- ? failure('git reset --hard HEAD', reset).error
401
- : cleaned.code !== 0
402
- ? failure('git ls-files --unmerged', cleaned).error
403
- : 'unmerged index entries remain after git reset --hard HEAD';
404
- return {
405
- success: false,
406
- committed,
407
- pushed: false,
408
- commit: null,
409
- timedOut: reset.timedOut || cleaned.timedOut,
410
- skipped: null,
411
- error: `autostash pop conflicted; push refused, but cleanup failed: ${cleanup}. Original tracked changes remain safe in ${stash.label}`,
412
- };
413
- }
414
- return {
415
- success: false,
416
- committed,
417
- pushed: false,
418
- commit: null,
419
- timedOut: false,
420
- skipped: null,
421
- error: `autostash pop conflicted; push refused and the worktree was reset to rebased HEAD without conflict markers. Original tracked changes remain safe in ${stash.label}; inspect with ${stash.ref ? `git stash show -p ${stash.ref}` : 'git stash list'}`,
422
- };
423
- }
424
- const push = await git(['push', '--', 'origin', branch]);
425
- if (push.code === 0) {
426
- const head = await git(['rev-parse', '--short=8', 'HEAD']);
427
- return {
428
- success: true,
429
- committed,
430
- pushed: committed || push.stdout.trim().length > 0 || push.stderr.includes('->'),
431
- commit: head.code === 0 ? head.stdout.trim() : null,
432
- timedOut: false,
433
- skipped: null,
434
- error: null,
435
- untrackedCleared,
436
- untrackedBackedUp,
437
- untrackedBackupDir,
438
- };
439
- }
440
- if (attempt === FLEET_SHARED_REPO_PUSH_ATTEMPTS || !isPushRace(push)) {
441
- return { ...failure('git push', push), committed };
442
- }
443
- }
444
- throw new Error('unreachable fleet shared repo push loop');
445
- }
446
- /**
447
- * Commit/pull/push this device's shared state under one cross-process lock.
448
- * Callers publish their local fields first and consume peer fields only after
449
- * this resolves successfully.
450
- */
451
- export async function syncFleetSharedStateRepo(options = {}) {
452
- const root = options.userAgentsDir ?? getUserAgentsDir();
453
- const device = options.device ?? machineId();
454
- const timeoutMs = options.timeoutMs ?? FLEET_SHARED_REPO_SYNC_DEADLINE_MS;
455
- const lockPath = options.lockPath ?? path.join(getDaemonDir(), 'fleet-shared-repo-sync');
456
- ensureLockTarget(lockPath, '');
457
- let release = null;
458
- try {
459
- release = await lockfile.lock(lockPath, {
460
- stale: Math.max(10_000, timeoutMs + 5_000),
461
- retries: { retries: 20, factor: 1, minTimeout: 100, maxTimeout: 100 },
462
- onCompromised: logAndContinueOnLockCompromised('fleet shared repo sync'),
463
- });
464
- const result = await performFleetSharedRepoSync(root, device, timeoutMs);
465
- // Record freshness only on a real success, beside the lock so a test-redirected
466
- // lockPath keeps its marker isolated too (PHNX-4051).
467
- if (result.success)
468
- writeExchangeMarker(lockPath);
469
- return result;
470
- }
471
- catch (err) {
472
- return {
473
- success: false,
474
- committed: false,
475
- pushed: false,
476
- commit: null,
477
- timedOut: false,
478
- skipped: null,
479
- error: err.message,
480
- };
481
- }
482
- finally {
483
- if (release) {
484
- try {
485
- await release();
486
- }
487
- catch { /* compromised/already released */ }
488
- }
489
- }
490
- }