@rubytech/create-sitedesk-code 0.1.514 → 0.1.516

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 (220) hide show
  1. package/dist/__tests__/launchd-bootstrap.test.js +164 -18
  2. package/dist/index.js +15 -14
  3. package/dist/launchd-bootstrap.js +86 -17
  4. package/package.json +1 -1
  5. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +624 -0
  6. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +139 -0
  7. package/payload/platform/plugins/admin/mcp/dist/index.js +14 -16
  8. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  9. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts +14 -0
  10. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts.map +1 -0
  11. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js +57 -0
  12. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js.map +1 -0
  13. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +66 -5
  14. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  15. package/payload/platform/plugins/docs/references/admin-ui.md +38 -3
  16. package/payload/platform/plugins/docs/references/deployment.md +4 -0
  17. package/payload/platform/plugins/email/PLUGIN.md +1 -1
  18. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.d.ts +2 -0
  19. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js +94 -0
  21. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js.map +1 -0
  22. package/payload/platform/plugins/email/mcp/dist/index.js +1 -1
  23. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  24. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +21 -1
  25. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  26. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +49 -8
  27. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  28. package/payload/platform/plugins/email/references/email-reference.md +4 -0
  29. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js +143 -2
  30. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js.map +1 -1
  31. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +33 -3
  32. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
  33. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +142 -0
  34. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -1
  35. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +8 -0
  36. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
  37. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +40 -0
  38. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
  39. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -1
  40. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +54 -3
  41. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
  42. package/payload/platform/plugins/outlook/references/graph-surfaces.md +9 -3
  43. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -2
  44. package/payload/platform/scripts/__tests__/agents-md-bootstrap.test.sh +47 -0
  45. package/payload/platform/scripts/lib/agents-md-bootstrap.sh +58 -1
  46. package/payload/platform/services/claude-session-manager/dist/config.d.ts +48 -12
  47. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/config.js +65 -15
  49. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/index.js +7 -4
  51. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js +17 -2
  54. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js.map +1 -1
  55. package/payload/server/public/activity.html +5 -5
  56. package/payload/server/public/agents.html +4 -4
  57. package/payload/server/public/assets/{AdminLoginScreens-Cs1jSnOF.js → AdminLoginScreens-B5qKb3YK.js} +1 -1
  58. package/payload/server/public/assets/AdminLoginScreens-B5qKb3YK.js.br +0 -0
  59. package/payload/server/public/assets/AdminLoginScreens-B5qKb3YK.js.gz +0 -0
  60. package/payload/server/public/assets/{AdminShell-DkrURZnF.js → AdminShell-BgJEq2zJ.js} +1 -1
  61. package/payload/server/public/assets/AdminShell-BgJEq2zJ.js.br +0 -0
  62. package/payload/server/public/assets/AdminShell-BgJEq2zJ.js.gz +0 -0
  63. package/payload/server/public/assets/{activity-BgZISMuE.js → activity-DmnVUDYO.js} +1 -1
  64. package/payload/server/public/assets/activity-DmnVUDYO.js.br +0 -0
  65. package/payload/server/public/assets/activity-DmnVUDYO.js.gz +0 -0
  66. package/payload/server/public/assets/{admin-DjFysc3-.js → admin-Dpge4OG9.js} +1 -1
  67. package/payload/server/public/assets/admin-Dpge4OG9.js.br +0 -0
  68. package/payload/server/public/assets/admin-Dpge4OG9.js.gz +0 -0
  69. package/payload/server/public/assets/{agents-Bx-MSZW6.js → agents-Bmq9X7P3.js} +1 -1
  70. package/payload/server/public/assets/agents-Bmq9X7P3.js.br +0 -0
  71. package/payload/server/public/assets/agents-Bmq9X7P3.js.gz +0 -0
  72. package/payload/server/public/assets/{browser-1I7syhHu.js → browser-B1G-W80B.js} +1 -1
  73. package/payload/server/public/assets/browser-B1G-W80B.js.br +0 -0
  74. package/payload/server/public/assets/browser-B1G-W80B.js.gz +0 -0
  75. package/payload/server/public/assets/{calendar-D_04JIo1.js → calendar-CHgrOFIN.js} +1 -1
  76. package/payload/server/public/assets/calendar-CHgrOFIN.js.br +0 -0
  77. package/payload/server/public/assets/calendar-CHgrOFIN.js.gz +0 -0
  78. package/payload/server/public/assets/chat-CIwmmtwP.js +1 -0
  79. package/payload/server/public/assets/chat-CIwmmtwP.js.br +0 -0
  80. package/payload/server/public/assets/chat-CIwmmtwP.js.gz +0 -0
  81. package/payload/server/public/assets/chevron-left-EQjfaQ-n.js +1 -0
  82. package/payload/server/public/assets/chevron-left-EQjfaQ-n.js.br +0 -0
  83. package/payload/server/public/assets/chevron-right-Ct8jZUGP.js +1 -0
  84. package/payload/server/public/assets/chevron-right-Ct8jZUGP.js.br +0 -0
  85. package/payload/server/public/assets/clock-Cxz5ybck.js +1 -0
  86. package/payload/server/public/assets/clock-Cxz5ybck.js.br +0 -0
  87. package/payload/server/public/assets/clock-Cxz5ybck.js.gz +0 -0
  88. package/payload/server/public/assets/data-BQVXKVsJ.js +1 -0
  89. package/payload/server/public/assets/data-BQVXKVsJ.js.br +1 -0
  90. package/payload/server/public/assets/data-BQVXKVsJ.js.gz +0 -0
  91. package/payload/server/public/assets/{file-text-DkvKqHMx.js → file-text-BVqqaDun.js} +1 -1
  92. package/payload/server/public/assets/file-text-BVqqaDun.js.br +0 -0
  93. package/payload/server/public/assets/file-text-BVqqaDun.js.gz +0 -0
  94. package/payload/server/public/assets/{graph-CCZhrwZX.js → graph-CEcME0lp.js} +1 -1
  95. package/payload/server/public/assets/graph-CEcME0lp.js.br +0 -0
  96. package/payload/server/public/assets/graph-CEcME0lp.js.gz +0 -0
  97. package/payload/server/public/assets/{graph-labels-BJ7puQkX.js → graph-labels-Zl32_AOH.js} +1 -1
  98. package/payload/server/public/assets/graph-labels-Zl32_AOH.js.br +0 -0
  99. package/payload/server/public/assets/graph-labels-Zl32_AOH.js.gz +0 -0
  100. package/payload/server/public/assets/{maximize-2-Xdj804U1.js → maximize-2-rkkMyAhf.js} +1 -1
  101. package/payload/server/public/assets/maximize-2-rkkMyAhf.js.br +0 -0
  102. package/payload/server/public/assets/maximize-2-rkkMyAhf.js.gz +0 -0
  103. package/payload/server/public/assets/{operator-BkfNpBs7.js → operator-DlL023ch.js} +1 -1
  104. package/payload/server/public/assets/operator-DlL023ch.js.br +0 -0
  105. package/payload/server/public/assets/operator-DlL023ch.js.gz +0 -0
  106. package/payload/server/public/assets/{page-D5n8ShE7.js → page-BAmYYqxp.js} +1 -1
  107. package/payload/server/public/assets/page-BAmYYqxp.js.br +0 -0
  108. package/payload/server/public/assets/page-BAmYYqxp.js.gz +0 -0
  109. package/payload/server/public/assets/{page-BoYc9c4a.js → page-k0BhBkA2.js} +1 -1
  110. package/payload/server/public/assets/page-k0BhBkA2.js.br +0 -0
  111. package/payload/server/public/assets/page-k0BhBkA2.js.gz +0 -0
  112. package/payload/server/public/assets/{public-CX5XSrxD.js → public-Cqgfmo1D.js} +1 -1
  113. package/payload/server/public/assets/public-Cqgfmo1D.js.br +0 -0
  114. package/payload/server/public/assets/public-Cqgfmo1D.js.gz +0 -0
  115. package/payload/server/public/assets/{rotate-ccw-dh_OhL23.js → rotate-ccw-O3RmqoTy.js} +1 -1
  116. package/payload/server/public/assets/rotate-ccw-O3RmqoTy.js.br +0 -0
  117. package/payload/server/public/assets/rotate-ccw-O3RmqoTy.js.gz +0 -0
  118. package/payload/server/public/assets/{routines-bb7dxcrr.js → routines-DMYObMUy.js} +1 -1
  119. package/payload/server/public/assets/routines-DMYObMUy.js.br +0 -0
  120. package/payload/server/public/assets/routines-DMYObMUy.js.gz +0 -0
  121. package/payload/server/public/assets/{skills-TeMwxXmJ.js → skills-DON1BKu5.js} +1 -1
  122. package/payload/server/public/assets/skills-DON1BKu5.js.br +0 -0
  123. package/payload/server/public/assets/skills-DON1BKu5.js.gz +0 -0
  124. package/payload/server/public/assets/{tasks-BqBBJlkg.js → tasks-C2PhUSrf.js} +1 -1
  125. package/payload/server/public/assets/tasks-C2PhUSrf.js.br +0 -0
  126. package/payload/server/public/assets/tasks-C2PhUSrf.js.gz +0 -0
  127. package/payload/server/public/assets/{time-entry-format-Cy_T-wiD.js → time-entry-format-4zgmuQI0.js} +1 -1
  128. package/payload/server/public/assets/time-entry-format-4zgmuQI0.js.br +0 -0
  129. package/payload/server/public/assets/time-entry-format-4zgmuQI0.js.gz +0 -0
  130. package/payload/server/public/assets/{triangle-alert-D-bSbPnX.js → triangle-alert-By5D7wuI.js} +1 -1
  131. package/payload/server/public/assets/triangle-alert-By5D7wuI.js.br +0 -0
  132. package/payload/server/public/assets/triangle-alert-By5D7wuI.js.gz +0 -0
  133. package/payload/server/public/assets/{useCopyFeedback-L7L55nFl.js → useCopyFeedback-Dx0vtm9s.js} +1 -1
  134. package/payload/server/public/assets/useCopyFeedback-Dx0vtm9s.js.br +0 -0
  135. package/payload/server/public/assets/useCopyFeedback-Dx0vtm9s.js.gz +0 -0
  136. package/payload/server/public/assets/{useSubAccountSwitcher-BYl2c7xA.css → useSubAccountSwitcher-UQQDcj0B.css} +1 -1
  137. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
  138. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
  139. package/payload/server/public/assets/{useVoiceRecorder-MuZ7lpet.js → useVoiceRecorder-BgwOCCwj.js} +1 -1
  140. package/payload/server/public/assets/useVoiceRecorder-BgwOCCwj.js.br +0 -0
  141. package/payload/server/public/assets/useVoiceRecorder-BgwOCCwj.js.gz +0 -0
  142. package/payload/server/public/assets/{wrench-DUG1SZQB.js → wrench-CHfMwLza.js} +1 -1
  143. package/payload/server/public/assets/wrench-CHfMwLza.js.br +0 -0
  144. package/payload/server/public/assets/wrench-CHfMwLza.js.gz +0 -0
  145. package/payload/server/public/browser.html +4 -4
  146. package/payload/server/public/calendar.html +7 -7
  147. package/payload/server/public/chat.html +13 -13
  148. package/payload/server/public/data.html +11 -11
  149. package/payload/server/public/graph.html +9 -9
  150. package/payload/server/public/index.html +14 -14
  151. package/payload/server/public/operator.html +14 -14
  152. package/payload/server/public/public.html +13 -13
  153. package/payload/server/public/routines.html +6 -6
  154. package/payload/server/public/skills.html +5 -5
  155. package/payload/server/public/tasks.html +6 -6
  156. package/payload/server/server.js +139 -17
  157. package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.br +0 -0
  158. package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.gz +0 -0
  159. package/payload/server/public/assets/AdminShell-DkrURZnF.js.br +0 -0
  160. package/payload/server/public/assets/AdminShell-DkrURZnF.js.gz +0 -0
  161. package/payload/server/public/assets/activity-BgZISMuE.js.br +0 -0
  162. package/payload/server/public/assets/activity-BgZISMuE.js.gz +0 -0
  163. package/payload/server/public/assets/admin-DjFysc3-.js.br +0 -0
  164. package/payload/server/public/assets/admin-DjFysc3-.js.gz +0 -0
  165. package/payload/server/public/assets/agents-Bx-MSZW6.js.br +0 -0
  166. package/payload/server/public/assets/agents-Bx-MSZW6.js.gz +0 -0
  167. package/payload/server/public/assets/browser-1I7syhHu.js.br +0 -0
  168. package/payload/server/public/assets/browser-1I7syhHu.js.gz +0 -0
  169. package/payload/server/public/assets/calendar-D_04JIo1.js.br +0 -0
  170. package/payload/server/public/assets/calendar-D_04JIo1.js.gz +0 -0
  171. package/payload/server/public/assets/chat-BFH3FM5a.js +0 -1
  172. package/payload/server/public/assets/chat-BFH3FM5a.js.br +0 -0
  173. package/payload/server/public/assets/chat-BFH3FM5a.js.gz +0 -0
  174. package/payload/server/public/assets/chevron-left-Ux9-qLkk.js +0 -1
  175. package/payload/server/public/assets/chevron-right-BwIuCfyG.js +0 -1
  176. package/payload/server/public/assets/chevron-right-BwIuCfyG.js.br +0 -0
  177. package/payload/server/public/assets/clock-DhJdZRhC.js +0 -1
  178. package/payload/server/public/assets/clock-DhJdZRhC.js.br +0 -0
  179. package/payload/server/public/assets/data-ChY_uSIk.js +0 -1
  180. package/payload/server/public/assets/data-ChY_uSIk.js.br +0 -0
  181. package/payload/server/public/assets/data-ChY_uSIk.js.gz +0 -0
  182. package/payload/server/public/assets/file-text-DkvKqHMx.js.br +0 -0
  183. package/payload/server/public/assets/file-text-DkvKqHMx.js.gz +0 -0
  184. package/payload/server/public/assets/graph-CCZhrwZX.js.br +0 -0
  185. package/payload/server/public/assets/graph-CCZhrwZX.js.gz +0 -0
  186. package/payload/server/public/assets/graph-labels-BJ7puQkX.js.br +0 -0
  187. package/payload/server/public/assets/graph-labels-BJ7puQkX.js.gz +0 -0
  188. package/payload/server/public/assets/maximize-2-Xdj804U1.js.br +0 -0
  189. package/payload/server/public/assets/maximize-2-Xdj804U1.js.gz +0 -0
  190. package/payload/server/public/assets/operator-BkfNpBs7.js.br +0 -0
  191. package/payload/server/public/assets/operator-BkfNpBs7.js.gz +0 -0
  192. package/payload/server/public/assets/page-BoYc9c4a.js.br +0 -0
  193. package/payload/server/public/assets/page-BoYc9c4a.js.gz +0 -0
  194. package/payload/server/public/assets/page-D5n8ShE7.js.br +0 -0
  195. package/payload/server/public/assets/page-D5n8ShE7.js.gz +0 -0
  196. package/payload/server/public/assets/public-CX5XSrxD.js.br +0 -0
  197. package/payload/server/public/assets/public-CX5XSrxD.js.gz +0 -0
  198. package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.br +0 -0
  199. package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.gz +0 -0
  200. package/payload/server/public/assets/routines-bb7dxcrr.js.br +0 -0
  201. package/payload/server/public/assets/routines-bb7dxcrr.js.gz +0 -0
  202. package/payload/server/public/assets/skills-TeMwxXmJ.js.br +0 -0
  203. package/payload/server/public/assets/skills-TeMwxXmJ.js.gz +0 -0
  204. package/payload/server/public/assets/tasks-BqBBJlkg.js.br +0 -0
  205. package/payload/server/public/assets/tasks-BqBBJlkg.js.gz +0 -0
  206. package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.br +0 -3
  207. package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.gz +0 -0
  208. package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.br +0 -1
  209. package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.gz +0 -0
  210. package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.br +0 -0
  211. package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.gz +0 -0
  212. package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.br +0 -0
  213. package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.gz +0 -0
  214. package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.br +0 -0
  215. package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.gz +0 -0
  216. package/payload/server/public/assets/wrench-DUG1SZQB.js.br +0 -0
  217. package/payload/server/public/assets/wrench-DUG1SZQB.js.gz +0 -0
  218. /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js → useSubAccountSwitcher-CRWoQXuU.js} +0 -0
  219. /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js.br → useSubAccountSwitcher-CRWoQXuU.js.br} +0 -0
  220. /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js.gz → useSubAccountSwitcher-CRWoQXuU.js.gz} +0 -0
@@ -1,10 +1,12 @@
1
- // acceptance grid for launchd-bootstrap.ts (Task 1401).
1
+ // acceptance grid for launchd-bootstrap.ts (Task 1401, reshaped by Task 2069).
2
2
  //
3
- // Locks the darwin session-manager bootstrap contract: bootout (idempotent,
4
- // rc 3 "No such process" ignored) then bootstrap, with a bounded retry on
5
- // exit 5 ("Input/output error" a known transient/racey launchctl failure on
6
- // re-install). Pure logic the launchctl runner and the retry sleep are both
7
- // injected, so no real launchctl or wall-clock delay runs here.
3
+ // Locks the darwin LaunchAgent bootstrap contract: bootout (idempotent, rc 3
4
+ // "No such process" ignored), then poll `launchctl print` until the label
5
+ // leaves the domain, then bootstrap, with a bounded retry on exit 5
6
+ // ("Input/output error"returned for as long as the previous instance is
7
+ // still terminating). Pure logic — the launchctl runner, the sleep and the
8
+ // clock are all injected, so no real launchctl and no wall-clock delay runs
9
+ // here.
8
10
  //
9
11
  // Compiles to dist/__tests__/launchd-bootstrap.test.js so
10
12
  // `node --test dist/__tests__/*.test.js` picks it up after build.
@@ -13,33 +15,74 @@ import assert from "node:assert/strict";
13
15
  import { bootstrapLaunchAgent } from "../launchd-bootstrap.js";
14
16
  const OPTS = { gui: "gui/502", label: "com.rubytech.host-claude-session-manager", plistPath: "/tmp/sm.plist" };
15
17
  const noSleep = () => { };
16
- test("first bootstrap succeeds one bootout, one bootstrap, attempts=1", () => {
18
+ /** A fake clock the injected sleepFn advances, so the wait-budget arithmetic
19
+ * runs with no wall-clock cost. */
20
+ function fakeClock() {
21
+ let t = 0;
22
+ return { now: () => t, sleep: (ms) => { t += ms; } };
23
+ }
24
+ test("waits for the label to leave the domain before bootstrapping", () => {
25
+ const clock = fakeClock();
17
26
  const calls = [];
27
+ let prints = 0;
18
28
  const run = (args) => {
19
29
  calls.push(args);
20
- return args[0] === "bootstrap" ? { status: 0, stderr: "" } : { status: 3, stderr: "No such process" };
30
+ if (args[0] === "print") {
31
+ prints += 1;
32
+ // The old instance lingers for 3 polls, then leaves the domain.
33
+ return prints <= 3 ? { status: 0, stderr: "" } : { status: 113, stderr: "" };
34
+ }
35
+ if (args[0] === "bootstrap")
36
+ return { status: 0, stderr: "" };
37
+ return { status: 0, stderr: "" };
21
38
  };
22
- const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: noSleep });
39
+ const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now });
23
40
  assert.equal(r.ok, true);
24
41
  assert.equal(r.attempts, 1);
42
+ assert.equal(prints, 4); // 3 polls with the label present, 1 showing it gone
43
+ assert.equal(r.totalWaitedMs, 1500); // 3 sleeps of 500ms before the gone poll
44
+ const bootstrapIdx = calls.findIndex((c) => c[0] === "bootstrap");
45
+ assert.ok(bootstrapIdx > 3, `bootstrap issued at index ${bootstrapIdx}, before the label was gone`);
46
+ });
47
+ test("fresh install — bootout, one absence poll, bootstrap, attempts=1", () => {
48
+ const clock = fakeClock();
49
+ const calls = [];
50
+ const run = (args) => {
51
+ calls.push(args);
52
+ if (args[0] === "print")
53
+ return { status: 113, stderr: "" };
54
+ if (args[0] === "bootstrap")
55
+ return { status: 0, stderr: "" };
56
+ return { status: 3, stderr: "No such process" };
57
+ };
58
+ const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now });
59
+ assert.equal(r.ok, true);
60
+ assert.equal(r.attempts, 1);
61
+ assert.equal(r.totalWaitedMs, 0); // gone on the first poll — a fresh install never waits
25
62
  assert.deepEqual(calls[0], ["bootout", "gui/502/com.rubytech.host-claude-session-manager"]);
26
- assert.deepEqual(calls[1], ["bootstrap", "gui/502", "/tmp/sm.plist"]);
27
- assert.equal(calls.length, 2);
63
+ assert.deepEqual(calls[1], ["print", "gui/502/com.rubytech.host-claude-session-manager"]);
64
+ assert.deepEqual(calls[2], ["bootstrap", "gui/502", "/tmp/sm.plist"]);
65
+ assert.equal(calls.length, 3);
28
66
  });
29
67
  test("exit 5 twice then 0 — retries and reports the winning attempt", () => {
68
+ const clock = fakeClock();
30
69
  let bootstraps = 0;
31
70
  const sleeps = [];
32
71
  const run = (args) => {
72
+ if (args[0] === "print")
73
+ return { status: 113, stderr: "" };
33
74
  if (args[0] !== "bootstrap")
34
75
  return { status: 3, stderr: "No such process" };
35
76
  bootstraps += 1;
36
77
  return bootstraps < 3 ? { status: 5, stderr: "Bootstrap failed: 5: Input/output error" } : { status: 0, stderr: "" };
37
78
  };
38
- const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: (ms) => { sleeps.push(ms); } });
79
+ const r = bootstrapLaunchAgent(run, {
80
+ ...OPTS, sleepFn: (ms) => { sleeps.push(ms); clock.sleep(ms); }, nowFn: clock.now,
81
+ });
39
82
  assert.equal(r.ok, true);
40
83
  assert.equal(r.attempts, 3);
41
84
  assert.equal(bootstraps, 3);
42
- assert.equal(sleeps.length, 2); // slept before each of the two retries
85
+ assert.equal(sleeps.filter((ms) => ms === 500).length, 2); // slept before each of the two retries
43
86
  });
44
87
  test("exit 5 on every attempt — fails after the attempt cap, not before", () => {
45
88
  let bootstraps = 0;
@@ -69,17 +112,120 @@ test("non-5 non-zero bootstrap aborts immediately — no retry", () => {
69
112
  assert.equal(r.bootstrapStatus, 9);
70
113
  assert.equal(bootstraps, 1);
71
114
  });
72
- test("emits one structured log line per attempt with bootout and bootstrap rc", () => {
115
+ test("label never leaves the domain bootstraps anyway at the budget, wait is reported", () => {
116
+ const clock = fakeClock();
117
+ const lines = [];
118
+ const run = (args) => {
119
+ if (args[0] === "print")
120
+ return { status: 0, stderr: "" }; // always present
121
+ if (args[0] === "bootstrap")
122
+ return { status: 5, stderr: "Bootstrap failed: 5: Input/output error" };
123
+ return { status: 0, stderr: "" };
124
+ };
125
+ const r = bootstrapLaunchAgent(run, {
126
+ ...OPTS, maxAttempts: 1, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l),
127
+ });
128
+ assert.equal(r.ok, false);
129
+ assert.equal(r.lastPresent, true);
130
+ assert.equal(r.totalWaitedMs, 60_000);
131
+ assert.ok(lines.some((l) => /op=wait-timeout waitedMs=60000 polls=\d+ present=true probeFailures=0 action=bootstrap-anyway/.test(l)));
132
+ });
133
+ test("the wait budget is shared across attempts, not spent per attempt", () => {
134
+ const clock = fakeClock();
135
+ const run = (args) => {
136
+ if (args[0] === "print")
137
+ return { status: 0, stderr: "" };
138
+ if (args[0] === "bootstrap")
139
+ return { status: 5, stderr: "eio" };
140
+ return { status: 0, stderr: "" };
141
+ };
142
+ const r = bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 3, sleepFn: clock.sleep, nowFn: clock.now });
143
+ assert.equal(r.attempts, 3);
144
+ // 60s of waiting in total across three attempts, not 180s.
145
+ assert.equal(r.totalWaitedMs, 60_000);
146
+ });
147
+ test("each exit-5 retry re-enters the wait rather than firing after retryDelayMs", () => {
148
+ const clock = fakeClock();
149
+ const seq = [];
150
+ let prints = 0;
151
+ const run = (args) => {
152
+ seq.push(args[0]);
153
+ if (args[0] === "print") {
154
+ prints += 1;
155
+ // present for one poll per attempt, then gone
156
+ return prints % 2 === 1 ? { status: 0, stderr: "" } : { status: 113, stderr: "" };
157
+ }
158
+ if (args[0] === "bootstrap")
159
+ return { status: 5, stderr: "eio" };
160
+ return { status: 0, stderr: "" };
161
+ };
162
+ bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 2, sleepFn: clock.sleep, nowFn: clock.now });
163
+ assert.deepEqual(seq, [
164
+ "bootout", "print", "print", "bootstrap",
165
+ "bootout", "print", "print", "bootstrap",
166
+ ]);
167
+ });
168
+ test("a probe that cannot run is not read as an absent label", () => {
169
+ const clock = fakeClock();
170
+ const lines = [];
171
+ const run = (args) => {
172
+ // status null = launchctl could not be spawned, or was killed by the
173
+ // spawnSync timeout. That is not evidence the label is gone.
174
+ if (args[0] === "print")
175
+ return { status: null, stderr: "" };
176
+ if (args[0] === "bootstrap")
177
+ return { status: 5, stderr: "eio" };
178
+ return { status: 0, stderr: "" };
179
+ };
180
+ const r = bootstrapLaunchAgent(run, {
181
+ ...OPTS, maxAttempts: 1, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l),
182
+ });
183
+ assert.equal(r.lastPresent, true); // never confirmed gone
184
+ assert.equal(r.totalWaitedMs, 60_000); // the full wait was served, not skipped
185
+ assert.ok(r.probeFailures > 0);
186
+ assert.ok(lines.some((l) => /op=wait-poll present=true probeRc=null/.test(l)));
187
+ });
188
+ test("the wait terminates on the poll cap even if the clock never advances", () => {
189
+ // A wall clock that steps backwards (an NTP correction) would leave the
190
+ // elapsed-time test permanently unsatisfied. Termination must not depend on
191
+ // time moving forwards.
192
+ const frozenNow = () => 0;
193
+ let polls = 0;
194
+ const run = (args) => {
195
+ if (args[0] === "print") {
196
+ polls += 1;
197
+ return { status: 0, stderr: "" };
198
+ }
199
+ if (args[0] === "bootstrap")
200
+ return { status: 5, stderr: "eio" };
201
+ return { status: 0, stderr: "" };
202
+ };
203
+ const r = bootstrapLaunchAgent(run, {
204
+ ...OPTS, maxAttempts: 1, sleepFn: noSleep, nowFn: frozenNow,
205
+ });
206
+ assert.equal(r.ok, false);
207
+ assert.equal(r.totalWaitedMs, 0); // the frozen clock measures no elapsed time
208
+ assert.equal(polls, 121); // ceil(60000/500) + 1 — the cap, not an infinite loop
209
+ });
210
+ test("emits one structured line per lifecycle step, correlated by agent label", () => {
211
+ const clock = fakeClock();
73
212
  let bootstraps = 0;
74
213
  const lines = [];
75
214
  const run = (args) => {
215
+ if (args[0] === "print")
216
+ return { status: 113, stderr: "" };
76
217
  if (args[0] !== "bootstrap")
77
218
  return { status: 3, stderr: "No such process" };
78
219
  bootstraps += 1;
79
220
  return bootstraps < 2 ? { status: 5, stderr: "eio" } : { status: 0, stderr: "" };
80
221
  };
81
- bootstrapLaunchAgent(run, { ...OPTS, sleepFn: noSleep, logger: (l) => lines.push(l) });
82
- assert.equal(lines.length, 2);
83
- assert.match(lines[0], /agent=com\.rubytech\.host-claude-session-manager bootout=3 bootstrap=5 attempt=1/);
84
- assert.match(lines[1], /bootout=3 bootstrap=0 attempt=2/);
222
+ bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l) });
223
+ const ops = lines.map((l) => l.match(/op=([a-z-]+)/)[1]);
224
+ assert.deepEqual(ops, [
225
+ "bootout", "wait-poll", "gone", "bootstrap",
226
+ "bootout", "wait-poll", "gone", "bootstrap", "result",
227
+ ]);
228
+ assert.ok(lines.every((l) => l.startsWith("[launchd] agent=com.rubytech.host-claude-session-manager ")));
229
+ assert.match(lines[3], /op=bootstrap rc=5 attempt=1 waitedMs=0/);
230
+ assert.match(lines[8], /op=result ok=true attempts=2 totalWaitedMs=0 probeFailures=0 rc=0/);
85
231
  });
package/dist/index.js CHANGED
@@ -1361,8 +1361,9 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
1361
1361
  const label = `${launchdLabel()}-neo4j`;
1362
1362
  console.log(" Resetting dedicated Neo4j auth (launchd)...");
1363
1363
  spawnSync("launchctl", ["bootout", `${gui()}/${label}`], { stdio: "pipe" });
1364
- // launchctl bootout returns once the job leaves the domain, but neo4j does a
1365
- // graceful multi-second store flush on SIGTERM — so the JVM can still hold
1364
+ // launchctl bootout returns as soon as launchd accepts the request (Task
1365
+ // 2069), and neo4j does a graceful multi-second store flush on SIGTERM — so
1366
+ // the JVM can still hold
1366
1367
  // the port (and the store) after bootout returns. Wait for the bolt port to
1367
1368
  // free before clearing the auth db and re-seeding: set-initial-password only
1368
1369
  // takes effect against a stopped store, and rm must not race a live flush.
@@ -1386,7 +1387,7 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
1386
1387
  const plistPath = join(launchAgentsDir(), `${label}.plist`);
1387
1388
  const boot = bootstrapLaunchAgent((a) => { const r = spawnSync("launchctl", a, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 }); return { status: r.status, stderr: (r.stderr ?? "").toString() }; }, { gui: gui(), label, plistPath, logger: (l) => { console.log(` ${l}`); logFile(` ${l}`); } });
1388
1389
  if (!boot.ok) {
1389
- throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s) during dedicated darwin auth reset: ${boot.stderr}`);
1390
+ throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s), waited ${boot.totalWaitedMs}ms, stillPresent=${boot.lastPresent}, probeFailures=${boot.probeFailures} during dedicated darwin auth reset: ${boot.stderr}`);
1390
1391
  }
1391
1392
  logFile(` [neo4j-darwin-auth-reset] dedicated datadir=${dataDir} conf=${p.confDir} label=${label}`);
1392
1393
  }
@@ -1804,7 +1805,7 @@ function setupDedicatedNeo4jDarwin() {
1804
1805
  logFile(` ${plistPath} written (${plist.length} bytes)`);
1805
1806
  const boot = bootstrapLaunchAgent((a) => { const r = spawnSync("launchctl", a, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 }); return { status: r.status, stderr: (r.stderr ?? "").toString() }; }, { gui: gui(), label, plistPath, logger: (l) => { console.log(` ${l}`); logFile(` ${l}`); } });
1806
1807
  if (!boot.ok) {
1807
- throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s): ${boot.stderr}`);
1808
+ throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s), waited ${boot.totalWaitedMs}ms, stillPresent=${boot.lastPresent}, probeFailures=${boot.probeFailures}: ${boot.stderr}`);
1808
1809
  }
1809
1810
  // Health-check the brand's bolt port — loud failure, no silent fallback to the
1810
1811
  // shared instance (matches the Linux dedicated bind check).
@@ -3891,12 +3892,12 @@ function installServiceDarwin() {
3891
3892
  });
3892
3893
  if (smBoot.ok) {
3893
3894
  console.log(` [launchd] bootstrap ${gui()}/${smLabel} ok`);
3894
- logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts}`);
3895
+ logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts} waitedMs=${smBoot.totalWaitedMs}`);
3895
3896
  }
3896
3897
  else {
3897
- console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
3898
- logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts}`);
3899
- throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
3898
+ console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s), waited ${smBoot.totalWaitedMs}ms, stillPresent=${smBoot.lastPresent}, probeFailures=${smBoot.probeFailures}: ${smBoot.stderr}`);
3899
+ logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts} waitedMs=${smBoot.totalWaitedMs} stillPresent=${smBoot.lastPresent} probeFailures=${smBoot.probeFailures}`);
3900
+ throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s), waited ${smBoot.totalWaitedMs}ms, stillPresent=${smBoot.lastPresent}, probeFailures=${smBoot.probeFailures}: ${smBoot.stderr}`);
3900
3901
  }
3901
3902
  // Health-probe the manager (/healthz) so a silent boot failure surfaces at
3902
3903
  // install time, not at first session spawn as `manager-unreachable`.
@@ -3947,11 +3948,11 @@ function installServiceDarwin() {
3947
3948
  logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
3948
3949
  });
3949
3950
  if (hbBoot.ok) {
3950
- logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts}`);
3951
+ logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts} waitedMs=${hbBoot.totalWaitedMs}`);
3951
3952
  }
3952
3953
  else {
3953
- console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s): ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
3954
- logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts}`);
3954
+ console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s), waited ${hbBoot.totalWaitedMs}ms, stillPresent=${hbBoot.lastPresent}, probeFailures=${hbBoot.probeFailures}: ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
3955
+ logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts} waitedMs=${hbBoot.totalWaitedMs} stillPresent=${hbBoot.lastPresent} probeFailures=${hbBoot.probeFailures}`);
3955
3956
  }
3956
3957
  // Task 1706 — the log rotator. On Linux a */15 entry in the per-brand cron
3957
3958
  // block ticks logs-rotate.sh (installCrons/buildBrandCronBlock); darwin has no
@@ -3987,11 +3988,11 @@ function installServiceDarwin() {
3987
3988
  logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
3988
3989
  });
3989
3990
  if (lrBoot.ok) {
3990
- logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts}`);
3991
+ logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts} waitedMs=${lrBoot.totalWaitedMs}`);
3991
3992
  }
3992
3993
  else {
3993
- console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s): ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
3994
- logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts}`);
3994
+ console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s), waited ${lrBoot.totalWaitedMs}ms, stillPresent=${lrBoot.lastPresent}, probeFailures=${lrBoot.probeFailures}: ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
3995
+ logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts} waitedMs=${lrBoot.totalWaitedMs} stillPresent=${lrBoot.lastPresent} probeFailures=${lrBoot.probeFailures}`);
3995
3996
  }
3996
3997
  // Wait for the server to come up.
3997
3998
  console.log(" Waiting for web server...");
@@ -1,19 +1,34 @@
1
- // Task 1401 — darwin session-manager LaunchAgent bootstrap, made idempotent and
1
+ // Task 1401 / Task 2069 — darwin LaunchAgent bootstrap, made idempotent and
2
2
  // EIO-tolerant.
3
3
  //
4
- // `launchctl bootstrap` intermittently returns exit 5 ("Bootstrap failed: 5:
5
- // Input/output error") on a re-install even when the target label is not
6
- // loaded (a `bootout` of it returns 3, "No such process"). It is a known
7
- // transient/racey launchd failure: bootstrapping the same plist from a clean
8
- // state succeeds. The earlier phase-11 code issued a single bare `bootstrap`
9
- // and treated any non-zero exit as fatal, so one EIO aborted the whole install.
4
+ // `launchctl bootstrap` returns exit 5 ("Bootstrap failed: 5: Input/output
5
+ // error") when the label's previous instance has not finished terminating.
6
+ // 1401 read this as a transient racey failure and retried three times 500 ms
7
+ // apart; 2069 measured it instead. `launchctl bootout` returns as soon as
8
+ // launchd accepts the request, but the job's process keeps running — a
9
+ // dedicated Neo4j's JVM took 14.2 s to reach "Stopped." on the Muvin iMac while
10
+ // all three retries fired inside 1.043 s and every one got exit 5. The failure
11
+ // is deterministic for the length of that shutdown, not random.
10
12
  //
11
- // This helper bootout-then-bootstraps with a bounded retry on exit 5. Both the
12
- // launchctl runner and the retry sleep are injected, so the logic is unit-tested
13
- // with no real launchctl and no wall-clock delay. Kept synchronous to match the
14
- // spawnSync-based installer body (installService is a synchronous `void`).
15
- /** `launchctl bootstrap` exit code for "Input/output error" the transient,
16
- * racey failure this retry loop exists to absorb. */
13
+ // So this helper boots the label out, polls `launchctl print <gui>/<label>`
14
+ // until the label leaves the domain, and only then bootstraps. `print` returns
15
+ // 0 for the whole time the old process lives and non-zero once it exits
16
+ // (measured: rc 113), which makes it an exit-code probe rather than prose
17
+ // parsing. The exit-5 retry is kept for the residual case, and each retry
18
+ // re-enters the wait.
19
+ //
20
+ // What `print`'s exit code does NOT report is liveness: a job that loads and
21
+ // exits immediately still returns rc 0, with "state = not running" appearing
22
+ // only in stdout. Detecting a dead-on-arrival agent therefore needs a different
23
+ // primitive and is not attempted here.
24
+ //
25
+ // The launchctl runner, the sleep, and the clock are all injected, so the logic
26
+ // is unit-tested with no real launchctl and no wall-clock delay. Kept
27
+ // synchronous to match the spawnSync-based installer body (installService is a
28
+ // synchronous `void`).
29
+ /** `launchctl bootstrap` exit code for "Input/output error" — returned for as
30
+ * long as the label's previous instance is still terminating. The wait above
31
+ * is what removes that window; the retry covers what is left. */
17
32
  const BOOTSTRAP_EIO = 5;
18
33
  /** Blocking sleep with no imports, safe inside the synchronous installer. */
19
34
  function blockingSleep(ms) {
@@ -24,20 +39,69 @@ function blockingSleep(ms) {
24
39
  export function bootstrapLaunchAgent(run, o) {
25
40
  const maxAttempts = Math.max(1, o.maxAttempts ?? 3);
26
41
  const retryDelayMs = o.retryDelayMs ?? 500;
42
+ const waitBudgetMs = o.waitBudgetMs ?? 60_000;
43
+ const pollIntervalMs = Math.max(1, o.pollIntervalMs ?? 500);
44
+ const now = o.nowFn ?? Date.now;
27
45
  const sleepFn = o.sleepFn ?? blockingSleep;
28
46
  const log = o.logger ?? (() => { });
47
+ const target = `${o.gui}/${o.label}`;
48
+ const tag = `[launchd] agent=${o.label}`;
49
+ // The wall clock is not monotonic — an NTP step backwards during the wait
50
+ // would leave the elapsed-time test below permanently unsatisfied. This poll
51
+ // allowance bounds the loop independently of the clock, so termination never
52
+ // depends on time moving forwards.
53
+ let pollsRemaining = Math.ceil(waitBudgetMs / pollIntervalMs) + 1;
54
+ let totalWaitedMs = 0;
55
+ let lastPresent = false;
56
+ let probeFailures = 0;
29
57
  let attempt = 0;
30
58
  let last = { status: null, stderr: "" };
31
59
  for (attempt = 1; attempt <= maxAttempts; attempt++) {
32
60
  // Idempotent: bootout any prior instance so the following bootstrap starts
33
61
  // clean. rc 3 ("No such process") is the expected fresh case; it and any
34
62
  // other bootout rc are ignored — bootout is cleanup, not the verified op.
35
- const bootout = run(["bootout", `${o.gui}/${o.label}`]);
63
+ const bootout = run(["bootout", target]);
64
+ log(`${tag} op=bootout rc=${bootout.status ?? "null"} attempt=${attempt}`);
65
+ // bootout returns as soon as launchd accepts the request, but the job's
66
+ // process can keep running for seconds (Neo4j's JVM: 14.2 s measured on the
67
+ // Muvin iMac). `launchctl print` returns 0 for that whole window and
68
+ // non-zero once the process is gone, so it is the absence probe.
69
+ // Bootstrapping before that is what returns EIO.
70
+ const waitStart = now();
71
+ let present = true;
72
+ let polls = 0;
73
+ for (;;) {
74
+ const p = run(["print", target]);
75
+ polls += 1;
76
+ pollsRemaining -= 1;
77
+ // Three outcomes, not two. rc 0 = still in the domain. A non-zero rc
78
+ // (113) = gone. A null status means the probe could not run at all —
79
+ // that is not evidence of absence, so the wait continues rather than
80
+ // treating an unrunnable probe as a clear domain.
81
+ const gone = p.status !== null && p.status !== 0;
82
+ if (p.status === null)
83
+ probeFailures += 1;
84
+ present = !gone;
85
+ const elapsedMs = now() - waitStart;
86
+ log(`${tag} op=wait-poll present=${present} probeRc=${p.status ?? "null"} elapsedMs=${elapsedMs}`);
87
+ if (gone) {
88
+ log(`${tag} op=gone goneMs=${elapsedMs} polls=${polls} attempt=${attempt}`);
89
+ break;
90
+ }
91
+ if (totalWaitedMs + elapsedMs >= waitBudgetMs || pollsRemaining <= 0) {
92
+ log(`${tag} op=wait-timeout waitedMs=${totalWaitedMs + elapsedMs} polls=${polls} present=true probeFailures=${probeFailures} action=bootstrap-anyway`);
93
+ break;
94
+ }
95
+ sleepFn(pollIntervalMs);
96
+ }
97
+ totalWaitedMs += Math.max(0, now() - waitStart);
98
+ lastPresent = present;
36
99
  const bootstrap = run(["bootstrap", o.gui, o.plistPath]);
37
100
  last = { status: bootstrap.status, stderr: (bootstrap.stderr ?? "").trim() };
38
- log(`[launchd] agent=${o.label} bootout=${bootout.status ?? "null"} bootstrap=${bootstrap.status ?? "null"} attempt=${attempt}`);
101
+ log(`${tag} op=bootstrap rc=${bootstrap.status ?? "null"} attempt=${attempt} waitedMs=${totalWaitedMs}`);
39
102
  if (bootstrap.status === 0) {
40
- return { ok: true, attempts: attempt, bootstrapStatus: 0, stderr: last.stderr };
103
+ log(`${tag} op=result ok=true attempts=${attempt} totalWaitedMs=${totalWaitedMs} probeFailures=${probeFailures} rc=0`);
104
+ return { ok: true, attempts: attempt, bootstrapStatus: 0, stderr: last.stderr, totalWaitedMs, lastPresent, probeFailures };
41
105
  }
42
106
  // Only exit 5 (EIO) is retried, and only while attempts remain. Every other
43
107
  // non-zero status is a real failure that aborts now rather than burning the
@@ -48,10 +112,15 @@ export function bootstrapLaunchAgent(run, o) {
48
112
  }
49
113
  break;
50
114
  }
115
+ const attempts = Math.min(attempt, maxAttempts);
116
+ log(`${tag} op=result ok=false attempts=${attempts} totalWaitedMs=${totalWaitedMs} probeFailures=${probeFailures} rc=${last.status ?? "null"}`);
51
117
  return {
52
118
  ok: false,
53
- attempts: Math.min(attempt, maxAttempts),
119
+ attempts,
54
120
  bootstrapStatus: last.status,
55
121
  stderr: last.stderr,
122
+ totalWaitedMs,
123
+ lastPresent,
124
+ probeFailures,
56
125
  };
57
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-sitedesk-code",
3
- "version": "0.1.514",
3
+ "version": "0.1.516",
4
4
  "description": "Install SiteDesk — automated back office for independent building contractors",
5
5
  "bin": {
6
6
  "create-sitedesk-code": "./dist/index.js"