@agimon-ai/po 0.0.0-alpha.2 → 0.0.0-alpha.21

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 (255) hide show
  1. package/README.md +14 -11
  2. package/dist/antigravity.mjs +1 -2
  3. package/dist/baseCommand.mjs +1 -2
  4. package/dist/bin/cli.cjs +37 -14
  5. package/dist/bin/cli.cjs.map +1 -1
  6. package/dist/bin/cli.mjs +34 -14
  7. package/dist/bin/cli.mjs.map +1 -1
  8. package/dist/bin/{doomRunner.cjs → poRunner.cjs} +5 -5
  9. package/dist/bin/poRunner.cjs.map +1 -0
  10. package/dist/bin/{doomRunner.d.mts → poRunner.d.mts} +2 -2
  11. package/dist/bin/poRunner.d.mts.map +1 -0
  12. package/dist/bin/{doomRunner.mjs → poRunner.mjs} +5 -5
  13. package/dist/bin/poRunner.mjs.map +1 -0
  14. package/dist/builders/cli/extensionAssembler/index.cjs +14 -12
  15. package/dist/builders/cli/extensionAssembler/index.cjs.map +1 -1
  16. package/dist/builders/cli/extensionAssembler/index.d.mts +2 -1
  17. package/dist/builders/cli/extensionAssembler/index.d.mts.map +1 -1
  18. package/dist/builders/cli/extensionAssembler/index.mjs +14 -12
  19. package/dist/builders/cli/extensionAssembler/index.mjs.map +1 -1
  20. package/dist/catalog/index.json +68 -51
  21. package/dist/cliApp.mjs +1 -2
  22. package/dist/codex.mjs +1 -2
  23. package/dist/compatibilityCommand.mjs +1 -2
  24. package/dist/compatibilityContext.mjs +1 -2
  25. package/dist/compatibilityOptions.mjs +1 -2
  26. package/dist/config.cjs +57 -12
  27. package/dist/config.d.mts +3 -3
  28. package/dist/config.mjs +3 -3
  29. package/dist/doctorCommand.mjs +1 -2
  30. package/dist/emitMcpCommand.mjs +1 -2
  31. package/dist/explainCommand.mjs +1 -2
  32. package/dist/extensions/server.cjs +11 -7
  33. package/dist/extensions/server.cjs.map +1 -1
  34. package/dist/extensions/server.d.mts +2 -2
  35. package/dist/extensions/server.d.mts.map +1 -1
  36. package/dist/extensions/server.mjs +12 -8
  37. package/dist/extensions/server.mjs.map +1 -1
  38. package/dist/harnessContext.mjs +1 -2
  39. package/dist/harnessState.cjs +66 -11
  40. package/dist/harnessState.d.mts +2 -2
  41. package/dist/harnessState.mjs +1 -2
  42. package/dist/index.cjs +60 -15
  43. package/dist/index.d.mts +3 -3
  44. package/dist/index.mjs +3 -3
  45. package/dist/initCommand.mjs +1 -2
  46. package/dist/matrixOptions.mjs +1 -2
  47. package/dist/options.mjs +1 -2
  48. package/dist/process.mjs +1 -2
  49. package/dist/projectTrust.cjs +2 -2
  50. package/dist/projectTrust.d.mts +2 -2
  51. package/dist/projectTrust.mjs +1 -2
  52. package/dist/repository.cjs +1 -1
  53. package/dist/repository.d.mts +2 -2
  54. package/dist/repository.mjs +1 -2
  55. package/dist/router.mjs +1 -2
  56. package/dist/shared.mjs +1 -2
  57. package/dist/src/bin/agentDirectory.cjs +18 -12
  58. package/dist/src/bin/agentDirectory.cjs.map +1 -1
  59. package/dist/src/bin/agentDirectory.mjs +15 -12
  60. package/dist/src/bin/agentDirectory.mjs.map +1 -1
  61. package/dist/src/builders/apiContractExport/index.cjs +6 -6
  62. package/dist/src/builders/apiContractExport/index.cjs.map +1 -1
  63. package/dist/src/builders/apiContractExport/index.mjs +9 -9
  64. package/dist/src/builders/apiContractExport/index.mjs.map +1 -1
  65. package/dist/src/builders/cli/entryResolution.cjs +3 -3
  66. package/dist/src/builders/cli/entryResolution.cjs.map +1 -1
  67. package/dist/src/builders/cli/entryResolution.mjs +3 -3
  68. package/dist/src/builders/cli/entryResolution.mjs.map +1 -1
  69. package/dist/src/builders/cli/harnessContext.cjs +5 -5
  70. package/dist/src/builders/cli/harnessContext.cjs.map +1 -1
  71. package/dist/src/builders/cli/harnessContext.mjs +7 -7
  72. package/dist/src/builders/cli/harnessContext.mjs.map +1 -1
  73. package/dist/src/builders/hooks/index.cjs +1 -1
  74. package/dist/src/builders/hooks/index.cjs.map +1 -1
  75. package/dist/src/builders/hooks/index.mjs +2 -2
  76. package/dist/src/builders/hooks/index.mjs.map +1 -1
  77. package/dist/src/builders/server/backgroundSync.cjs +13 -1
  78. package/dist/src/builders/server/backgroundSync.cjs.map +1 -1
  79. package/dist/src/builders/server/backgroundSync.mjs +13 -1
  80. package/dist/src/builders/server/backgroundSync.mjs.map +1 -1
  81. package/dist/src/builders/server/bundlerService.cjs +151 -0
  82. package/dist/src/builders/server/bundlerService.cjs.map +1 -0
  83. package/dist/src/builders/server/bundlerService.mjs +151 -0
  84. package/dist/src/builders/server/bundlerService.mjs.map +1 -0
  85. package/dist/src/builders/server/computerUseBinding.cjs +2 -2
  86. package/dist/src/builders/server/computerUseBinding.cjs.map +1 -1
  87. package/dist/src/builders/server/computerUseBinding.mjs +3 -3
  88. package/dist/src/builders/server/computerUseBinding.mjs.map +1 -1
  89. package/dist/src/builders/server/defaultWorkspace.cjs +1 -1
  90. package/dist/src/builders/server/defaultWorkspace.cjs.map +1 -1
  91. package/dist/src/builders/server/defaultWorkspace.mjs +2 -2
  92. package/dist/src/builders/server/defaultWorkspace.mjs.map +1 -1
  93. package/dist/src/builders/server/idleRestart.cjs +3 -3
  94. package/dist/src/builders/server/idleRestart.cjs.map +1 -1
  95. package/dist/src/builders/server/idleRestart.mjs +3 -3
  96. package/dist/src/builders/server/idleRestart.mjs.map +1 -1
  97. package/dist/src/builders/server/index.cjs +6 -6
  98. package/dist/src/builders/server/index.cjs.map +1 -1
  99. package/dist/src/builders/server/index.mjs +6 -6
  100. package/dist/src/builders/server/index.mjs.map +1 -1
  101. package/dist/src/builders/server/logSink.cjs +3 -3
  102. package/dist/src/builders/server/logSink.cjs.map +1 -1
  103. package/dist/src/builders/server/logSink.mjs +2 -2
  104. package/dist/src/builders/server/logSink.mjs.map +1 -1
  105. package/dist/src/builders/server/mcpBundle.cjs.map +1 -1
  106. package/dist/src/builders/server/mcpBundle.d.mts +2 -2
  107. package/dist/src/builders/server/mcpBundle.mjs.map +1 -1
  108. package/dist/src/builders/server/packageUrl.cjs +25 -18
  109. package/dist/src/builders/server/packageUrl.cjs.map +1 -1
  110. package/dist/src/builders/server/packageUrl.mjs +25 -18
  111. package/dist/src/builders/server/packageUrl.mjs.map +1 -1
  112. package/dist/src/builders/server/runtime.cjs +157 -168
  113. package/dist/src/builders/server/runtime.cjs.map +1 -1
  114. package/dist/src/builders/server/runtime.mjs +161 -171
  115. package/dist/src/builders/server/runtime.mjs.map +1 -1
  116. package/dist/src/builders/server/selectionStatus.cjs +3 -3
  117. package/dist/src/builders/server/selectionStatus.cjs.map +1 -1
  118. package/dist/src/builders/server/selectionStatus.mjs +3 -3
  119. package/dist/src/builders/server/selectionStatus.mjs.map +1 -1
  120. package/dist/src/builders/server/sessionArguments.cjs.map +1 -1
  121. package/dist/src/builders/server/sessionArguments.mjs.map +1 -1
  122. package/dist/src/builders/server/watcherService.cjs +65 -0
  123. package/dist/src/builders/server/watcherService.cjs.map +1 -0
  124. package/dist/src/builders/server/watcherService.mjs +61 -0
  125. package/dist/src/builders/server/watcherService.mjs.map +1 -0
  126. package/dist/src/builders/server/webTemplateDecision.cjs +1 -1
  127. package/dist/src/builders/server/webTemplateDecision.cjs.map +1 -1
  128. package/dist/src/builders/server/webTemplateDecision.mjs +2 -2
  129. package/dist/src/builders/server/webTemplateDecision.mjs.map +1 -1
  130. package/dist/src/builders/web/bundle.cjs.map +1 -1
  131. package/dist/src/builders/web/bundle.d.mts +4 -3
  132. package/dist/src/builders/web/bundle.mjs.map +1 -1
  133. package/dist/src/builders/web/bundleAssetPolicy.d.mts +1 -0
  134. package/dist/src/builders/web/index.cjs +1 -1
  135. package/dist/src/builders/web/index.cjs.map +1 -1
  136. package/dist/src/builders/web/index.d.mts +1 -0
  137. package/dist/src/builders/web/index.mjs +1 -1
  138. package/dist/src/builders/web/index.mjs.map +1 -1
  139. package/dist/src/builders/web/webPluginGenerate.cjs +4 -15
  140. package/dist/src/builders/web/webPluginGenerate.cjs.map +1 -1
  141. package/dist/src/builders/web/webPluginGenerate.d.mts +5 -9
  142. package/dist/src/builders/web/webPluginGenerate.d.mts.map +1 -1
  143. package/dist/src/builders/web/webPluginGenerate.mjs +4 -15
  144. package/dist/src/builders/web/webPluginGenerate.mjs.map +1 -1
  145. package/dist/src/builders/web/webPluginVite.d.mts +1 -0
  146. package/dist/src/cli/cliApp.cjs +1 -1
  147. package/dist/src/cli/cliApp.cjs.map +1 -1
  148. package/dist/src/cli/cliApp.mjs +1 -1
  149. package/dist/src/cli/cliApp.mjs.map +1 -1
  150. package/dist/src/cli/commands/compat/index.cjs +1 -1
  151. package/dist/src/cli/commands/compat/index.cjs.map +1 -1
  152. package/dist/src/cli/commands/compat/index.mjs +1 -1
  153. package/dist/src/cli/commands/compat/index.mjs.map +1 -1
  154. package/dist/src/cli/commands/doctor/index.cjs +2 -2
  155. package/dist/src/cli/commands/doctor/index.cjs.map +1 -1
  156. package/dist/src/cli/commands/doctor/index.d.mts +1 -0
  157. package/dist/src/cli/commands/doctor/index.d.mts.map +1 -1
  158. package/dist/src/cli/commands/doctor/index.mjs +4 -4
  159. package/dist/src/cli/commands/doctor/index.mjs.map +1 -1
  160. package/dist/src/cli/commands/explain/index.cjs +3 -1
  161. package/dist/src/cli/commands/explain/index.cjs.map +1 -1
  162. package/dist/src/cli/commands/explain/index.d.mts.map +1 -1
  163. package/dist/src/cli/commands/explain/index.mjs +4 -2
  164. package/dist/src/cli/commands/explain/index.mjs.map +1 -1
  165. package/dist/src/cli/commands/init/index.cjs +2 -2
  166. package/dist/src/cli/commands/init/index.cjs.map +1 -1
  167. package/dist/src/cli/commands/init/index.mjs +3 -3
  168. package/dist/src/cli/commands/init/index.mjs.map +1 -1
  169. package/dist/src/cli/commands/init/presenter.cjs +1 -1
  170. package/dist/src/cli/commands/init/presenter.cjs.map +1 -1
  171. package/dist/src/cli/commands/init/presenter.mjs +2 -2
  172. package/dist/src/cli/commands/init/presenter.mjs.map +1 -1
  173. package/dist/src/cli/commands/sync/index.cjs +4 -4
  174. package/dist/src/cli/commands/sync/index.cjs.map +1 -1
  175. package/dist/src/cli/commands/sync/index.d.mts +1 -0
  176. package/dist/src/cli/commands/sync/index.mjs +7 -7
  177. package/dist/src/cli/commands/sync/index.mjs.map +1 -1
  178. package/dist/src/cli/commands/sync/presenter.cjs +1 -1
  179. package/dist/src/cli/commands/sync/presenter.cjs.map +1 -1
  180. package/dist/src/cli/commands/sync/presenter.d.mts +1 -1
  181. package/dist/src/cli/commands/sync/presenter.mjs +1 -1
  182. package/dist/src/cli/commands/sync/presenter.mjs.map +1 -1
  183. package/dist/src/cli/commands/web/index.cjs +2 -1
  184. package/dist/src/cli/commands/web/index.cjs.map +1 -1
  185. package/dist/src/cli/commands/web/index.mjs +2 -1
  186. package/dist/src/cli/commands/web/index.mjs.map +1 -1
  187. package/dist/src/cli/harnessOptions.cjs +1 -1
  188. package/dist/src/cli/harnessOptions.cjs.map +1 -1
  189. package/dist/src/cli/harnessOptions.mjs +2 -2
  190. package/dist/src/cli/harnessOptions.mjs.map +1 -1
  191. package/dist/src/cli/help.cjs +2 -0
  192. package/dist/src/cli/help.cjs.map +1 -1
  193. package/dist/src/cli/help.mjs +2 -0
  194. package/dist/src/cli/help.mjs.map +1 -1
  195. package/dist/src/cli/matrixOptions.cjs +1 -1
  196. package/dist/src/cli/matrixOptions.cjs.map +1 -1
  197. package/dist/src/cli/matrixOptions.d.mts +1 -1
  198. package/dist/src/cli/matrixOptions.mjs +1 -1
  199. package/dist/src/cli/matrixOptions.mjs.map +1 -1
  200. package/dist/src/cli/server/run.cjs +1 -1
  201. package/dist/src/cli/server/run.cjs.map +1 -1
  202. package/dist/src/cli/server/run.mjs +2 -2
  203. package/dist/src/cli/server/run.mjs.map +1 -1
  204. package/dist/src/composition/harnessState/index.cjs +66 -11
  205. package/dist/src/composition/harnessState/index.cjs.map +1 -1
  206. package/dist/src/composition/harnessState/index.d.mts +3 -3
  207. package/dist/src/composition/harnessState/index.mjs +2 -2
  208. package/dist/src/composition/harnessState/index.mjs.map +1 -1
  209. package/dist/src/composition/layerPackageInstaller/index.cjs +5 -4
  210. package/dist/src/composition/layerPackageInstaller/index.cjs.map +1 -1
  211. package/dist/src/composition/layerPackageInstaller/index.mjs +6 -5
  212. package/dist/src/composition/layerPackageInstaller/index.mjs.map +1 -1
  213. package/dist/src/composition/poHomeMigration/index.cjs +1 -1
  214. package/dist/src/composition/poHomeMigration/index.cjs.map +1 -1
  215. package/dist/src/composition/poHomeMigration/index.mjs +1 -1
  216. package/dist/src/composition/poHomeMigration/index.mjs.map +1 -1
  217. package/dist/src/composition/projectTrust/index.cjs +6 -6
  218. package/dist/src/composition/projectTrust/index.cjs.map +1 -1
  219. package/dist/src/composition/projectTrust/index.d.mts +7 -7
  220. package/dist/src/composition/projectTrust/index.d.mts.map +1 -1
  221. package/dist/src/composition/projectTrust/index.mjs +6 -6
  222. package/dist/src/composition/projectTrust/index.mjs.map +1 -1
  223. package/dist/src/composition/repository/index.cjs +8 -8
  224. package/dist/src/composition/repository/index.cjs.map +1 -1
  225. package/dist/src/composition/repository/index.d.mts +5 -5
  226. package/dist/src/composition/repository/index.d.mts.map +1 -1
  227. package/dist/src/composition/repository/index.mjs +10 -10
  228. package/dist/src/composition/repository/index.mjs.map +1 -1
  229. package/dist/src/composition/syncService/index.cjs +29 -9
  230. package/dist/src/composition/syncService/index.cjs.map +1 -1
  231. package/dist/src/composition/syncService/index.d.mts.map +1 -1
  232. package/dist/src/composition/syncService/index.mjs +31 -11
  233. package/dist/src/composition/syncService/index.mjs.map +1 -1
  234. package/dist/src/composition/syncService/localBuilds.cjs +7 -2
  235. package/dist/src/composition/syncService/localBuilds.cjs.map +1 -1
  236. package/dist/src/composition/syncService/localBuilds.mjs +7 -2
  237. package/dist/src/composition/syncService/localBuilds.mjs.map +1 -1
  238. package/dist/src/extensions/setupDiagnostics.cjs +3 -3
  239. package/dist/src/extensions/setupDiagnostics.cjs.map +1 -1
  240. package/dist/src/extensions/setupDiagnostics.mjs +3 -3
  241. package/dist/src/extensions/setupDiagnostics.mjs.map +1 -1
  242. package/dist/utils.mjs +1 -2
  243. package/docs/server/api-export.md +3 -3
  244. package/docs/server/api.md +23 -22
  245. package/docs/server/getting-started.md +7 -7
  246. package/docs/server/index.md +4 -4
  247. package/docs/server/ipc.md +7 -11
  248. package/docs/server/lifecycle.md +17 -17
  249. package/docs/server/security.md +8 -8
  250. package/package.json +42 -45
  251. package/src/prompts/po-author-extension/SKILL.md +1 -1
  252. package/src/prompts/po-author-extension/references/extension-contract.md +4 -4
  253. package/dist/bin/doomRunner.cjs.map +0 -1
  254. package/dist/bin/doomRunner.d.mts.map +0 -1
  255. package/dist/bin/doomRunner.mjs.map +0 -1
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { resolvePluginEntries, resolveSharedSkills } from "@agimon-ai/po-config/domains";
6
- import { globalDoomConfigDirectory } from "@agimon-ai/po-config/config";
6
+ import { globalPoConfigDirectory } from "@agimon-ai/po-config/config";
7
7
  import { filterHookDisabledLayers, layerHookGroups, loadMajorModesConfig, resolveLayers } from "@agimon-ai/po-config/majorModes";
8
8
  import { applyProfileEnvironment, buildPersonaPrompt, resolveProfile } from "@agimon-ai/po-config/profiles";
9
9
  import { HARNESS_EVENT, createHarnessTelemetry } from "@agimon-ai/po-core/runtimeLogSinkTelemetry";
@@ -12,7 +12,7 @@ import { materializePluginEntries } from "@agimon-ai/po-domain/plugins";
12
12
  import { collectResources } from "@agimon-ai/po-domain/resources";
13
13
  import crypto from "node:crypto";
14
14
  import { DEFAULT_THEME_NAME, writeDefaultTheme } from "@agimon-ai/po-ui/theme";
15
- import { DOOM_MCP_SESSION_ENV_VAR } from "@agimon-ai/po-core/mcpSession";
15
+ import { PO_MCP_SESSION_ENV_VAR } from "@agimon-ai/po-core/mcpSession";
16
16
  //#region src/builders/cli/harnessContext.ts
17
17
  const PRIVATE_FILE_MODE = 384;
18
18
  const RESOLVE_CONFIG_PHASE = "resolve_config";
@@ -42,7 +42,7 @@ function resolveHarnessProfile(options) {
42
42
  */
43
43
  async function buildHarnessContext(options, telemetry = createHarnessTelemetry()) {
44
44
  const configRoot = options.configRoot ?? options.repoRoot;
45
- const configured = await telemetry.runInSpan(`doom_pi.${RESOLVE_CONFIG_PHASE}`, { "harness.phase": RESOLVE_CONFIG_PHASE }, async () => {
45
+ const configured = await telemetry.runInSpan(`po_pi.${RESOLVE_CONFIG_PHASE}`, { "harness.phase": RESOLVE_CONFIG_PHASE }, async () => {
46
46
  const majorModesConfig = loadMajorModesConfig(configRoot, options.homeDirectory, options.environment);
47
47
  const selectedLayers = filterHookDisabledLayers(majorModesConfig, resolveLayers(majorModesConfig, options.majorMode), options.hooks);
48
48
  const profile = resolveHarnessProfile(options);
@@ -67,7 +67,7 @@ async function buildHarnessContext(options, telemetry = createHarnessTelemetry()
67
67
  const { majorModesConfig, selectedLayers, hookGroups, profile, plugins, mcpAllowlist } = configured;
68
68
  const personaDirectory = profile?.persona;
69
69
  const personaRoot = profile?.personaRoot ?? configRoot;
70
- const resources = await telemetry.runInSpan(`doom_pi.${COLLECT_RESOURCES_PHASE}`, {
70
+ const resources = await telemetry.runInSpan(`po_pi.${COLLECT_RESOURCES_PHASE}`, {
71
71
  "harness.plugin.count": plugins.length,
72
72
  "harness.agents": options.agents,
73
73
  "harness.mcp": options.mcp
@@ -75,10 +75,10 @@ async function buildHarnessContext(options, telemetry = createHarnessTelemetry()
75
75
  agents: options.agents,
76
76
  mcp: options.mcp,
77
77
  temporaryDirectory: options.resourceDirectory,
78
- skillCacheDirectory: options.resourceDirectory === void 0 ? path.join(globalDoomConfigDirectory(options.homeDirectory ?? os.homedir()), "cache", "skills", crypto.createHash("sha256").update(path.resolve(options.repoRoot)).digest("hex").slice(0, 32)) : path.join(options.resourceDirectory, "cache", "skills"),
78
+ skillCacheDirectory: options.resourceDirectory === void 0 ? path.join(globalPoConfigDirectory(options.homeDirectory ?? os.homedir()), "cache", "skills", crypto.createHash("sha256").update(path.resolve(options.repoRoot)).digest("hex").slice(0, 32)) : path.join(options.resourceDirectory, "cache", "skills"),
79
79
  mcpAllowlist,
80
80
  sharedSkills: configured.sharedSkills,
81
- pluginDataRoot: path.join(globalDoomConfigDirectory(options.homeDirectory ?? os.homedir()), "plugin-data")
81
+ pluginDataRoot: path.join(globalPoConfigDirectory(options.homeDirectory ?? os.homedir()), "plugin-data")
82
82
  })).catch(async (error) => {
83
83
  await telemetry.recordError(HARNESS_EVENT.resourceCollectionFailed, error, {
84
84
  "harness.phase": COLLECT_RESOURCES_PHASE,
@@ -89,7 +89,7 @@ async function buildHarnessContext(options, telemetry = createHarnessTelemetry()
89
89
  try {
90
90
  const defaultThemePath = await writeDefaultTheme(resources.temporaryDirectory);
91
91
  const environment = { ...options.environment ?? process.env };
92
- delete environment[DOOM_MCP_SESSION_ENV_VAR];
92
+ delete environment[PO_MCP_SESSION_ENV_VAR];
93
93
  const profileEnvironment = applyProfileEnvironment(environment, profile?.env ?? {});
94
94
  configurePreset(options, environment);
95
95
  environment.NX_DAEMON ??= "false";
@@ -1 +1 @@
1
- {"version":3,"file":"harnessContext.mjs","names":[],"sources":["../../../../src/builders/cli/harnessContext.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { globalDoomConfigDirectory } from '@agimon-ai/po-config/config';\nimport { resolvePluginEntries, resolveSharedSkills } from '@agimon-ai/po-config/domains';\nimport type { DomainMcpAllowlist, PluginEntry } from '@agimon-ai/po-config/domains';\nimport {\n filterHookDisabledLayers,\n layerHookGroups,\n loadMajorModesConfig,\n resolveLayers,\n} from '@agimon-ai/po-config/majorModes';\nimport type { MajorModesConfig } from '@agimon-ai/po-config/majorModes';\nimport { applyProfileEnvironment, buildPersonaPrompt, resolveProfile } from '@agimon-ai/po-config/profiles';\nimport type { AgentProfile } from '@agimon-ai/po-config/profiles';\nimport { DOOM_MCP_SESSION_ENV_VAR } from '@agimon-ai/po-core/mcpSession';\nimport {\n createHarnessTelemetry,\n HARNESS_EVENT,\n type HarnessTelemetry,\n} from '@agimon-ai/po-core/runtimeLogSinkTelemetry';\nimport { resolveMcpAllowlist } from '@agimon-ai/po-domain/mcp';\nimport { materializePluginEntries } from '@agimon-ai/po-domain/plugins';\nimport { collectResources, type HarnessResources } from '@agimon-ai/po-domain/resources';\nimport { DEFAULT_THEME_NAME, writeDefaultTheme } from '@agimon-ai/po-ui/theme';\n\nimport { createHarnessSession } from '../../composition/harnessState';\nimport type { HarnessOptions } from '../../composition/types/harness';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst RESOLVE_CONFIG_PHASE = 'resolve_config';\nconst COLLECT_RESOURCES_PHASE = 'collect_resources';\nconst BUILD_CONTEXT_PHASE = 'build_context';\n\n/**\n * Everything a command needs, resolved once.\n *\n * All three commands need the same expensive preparation (domains resolved,\n * plugins collected into a temp directory, environment assembled), so it is\n * built here and handed to whichever command runs.\n */\nexport interface HarnessContext {\n options: HarnessOptions;\n environment: NodeJS.ProcessEnv;\n resources: HarnessResources;\n plugins: PluginEntry[];\n profile?: string;\n personaDirectory?: string;\n /** Root personaDirectory resolves against, when a profile supplied one. */\n personaRoot?: string;\n mcpAllowlist?: DomainMcpAllowlist;\n majorModesConfig: MajorModesConfig;\n selectedLayers: string[];\n hookGroups: string[];\n defaultThemePath: string;\n cleanup(): Promise<void>;\n}\n\n/**\n * Applies the provider environment a preset needs.\n *\n * Exported so a session server can apply the same rules before anything reads them.\n */\nexport function configurePreset(\n options: Pick<HarnessOptions, 'preset' | 'piArgs'>,\n environment: NodeJS.ProcessEnv,\n): void {\n if (options.preset === 'kimi') {\n if (!environment.KIMI_API_KEY && environment.KIMI_CODE_API_KEY) {\n environment.KIMI_API_KEY = environment.KIMI_CODE_API_KEY;\n }\n if (!environment.KIMI_API_KEY) {\n throw new Error('KIMI_API_KEY or KIMI_CODE_API_KEY is required for the kimi preset');\n }\n }\n}\n\n/** Resolves the optional persona and environment profile selected for this run. */\nexport function resolveHarnessProfile(options: HarnessOptions): AgentProfile | undefined {\n return options.profile ? resolveProfile(options.configRoot ?? options.repoRoot, options.profile) : undefined;\n}\n\n/**\n * Resolves the full matrix and stages every resource Pi will need.\n *\n * The caller owns the returned cleanup(): the temporary directory holding\n * skills, agents, and the generated MCP config has to outlive this function\n * and be removed once the selected command is done with it.\n */\nexport async function buildHarnessContext(\n options: HarnessOptions,\n telemetry: HarnessTelemetry = createHarnessTelemetry(),\n): Promise<HarnessContext> {\n // A worktree reads its workspace's configuration; resources still come from the checkout.\n const configRoot = options.configRoot ?? options.repoRoot;\n // Everything up to collectResources reads configuration off disk, so a single\n // malformed modes.yaml or profiles.yaml surfaces here as a failed launch\n // with no other record.\n const configured = await telemetry\n .runInSpan(`doom_pi.${RESOLVE_CONFIG_PHASE}`, { 'harness.phase': RESOLVE_CONFIG_PHASE }, async () => {\n const majorModesConfig = loadMajorModesConfig(configRoot, options.homeDirectory, options.environment);\n const selectedLayers = filterHookDisabledLayers(\n majorModesConfig,\n resolveLayers(majorModesConfig, options.majorMode),\n options.hooks,\n );\n const profile = resolveHarnessProfile(options);\n if (!fs.existsSync(options.cwd) || !fs.statSync(options.cwd).isDirectory()) {\n throw new Error(`Working directory does not exist: ${options.cwd}`);\n }\n return {\n majorModesConfig,\n selectedLayers,\n hookGroups: layerHookGroups(majorModesConfig, selectedLayers),\n profile,\n plugins: await materializePluginEntries(\n resolvePluginEntries(configRoot, options.domains, options.pluginDirectories),\n ),\n mcpAllowlist: resolveMcpAllowlist(configRoot, options.domains),\n sharedSkills: resolveSharedSkills(configRoot, options.domains),\n };\n })\n .catch(async (error: unknown) => {\n await telemetry.recordError(HARNESS_EVENT.configLoadFailed, error, {\n 'harness.phase': RESOLVE_CONFIG_PHASE,\n 'harness.major_mode': options.majorMode,\n 'harness.domain_count': options.domains.length,\n });\n throw error;\n });\n\n const { majorModesConfig, selectedLayers, hookGroups, profile, plugins, mcpAllowlist } = configured;\n const personaDirectory = profile?.persona;\n // A global profile's persona lives beside the global config, not in the repo.\n const personaRoot = profile?.personaRoot ?? configRoot;\n const resources = await telemetry\n .runInSpan(\n `doom_pi.${COLLECT_RESOURCES_PHASE}`,\n { 'harness.plugin.count': plugins.length, 'harness.agents': options.agents, 'harness.mcp': options.mcp },\n () =>\n collectResources(options.repoRoot, plugins, {\n agents: options.agents,\n mcp: options.mcp,\n // Set by a caller that stages into a directory it owns.\n temporaryDirectory: options.resourceDirectory,\n // Per repository, under the global home, so it outlives this process's resources.\n skillCacheDirectory:\n options.resourceDirectory === undefined\n ? path.join(\n globalDoomConfigDirectory(options.homeDirectory ?? os.homedir()),\n 'cache',\n 'skills',\n crypto.createHash('sha256').update(path.resolve(options.repoRoot)).digest('hex').slice(0, 32),\n )\n : path.join(options.resourceDirectory, 'cache', 'skills'),\n mcpAllowlist,\n sharedSkills: configured.sharedSkills,\n pluginDataRoot: path.join(globalDoomConfigDirectory(options.homeDirectory ?? os.homedir()), 'plugin-data'),\n }),\n )\n .catch(async (error: unknown) => {\n // Conflicting skills, agents, or MCP servers all fail here, and the plugin\n // that caused it is only identifiable from the message.\n await telemetry.recordError(HARNESS_EVENT.resourceCollectionFailed, error, {\n 'harness.phase': COLLECT_RESOURCES_PHASE,\n 'harness.plugin.count': plugins.length,\n });\n throw error;\n });\n\n try {\n const defaultThemePath = await writeDefaultTheme(resources.temporaryDirectory);\n const environment = { ...(options.environment ?? process.env) };\n // The file-backed projection published through Cordis is authoritative.\n // Never let a legacy parent-process projection leak into this Doom session.\n delete environment[DOOM_MCP_SESSION_ENV_VAR];\n const profileEnvironment = applyProfileEnvironment(environment, profile?.env ?? {});\n\n configurePreset(options, environment);\n environment.NX_DAEMON ??= 'false';\n environment.PO_THEME ||= DEFAULT_THEME_NAME;\n environment.CLAUDE_PROJECT_DIR = options.repoRoot;\n environment.CODEX_REPO_ROOT = options.repoRoot;\n environment.ORIGINAL_REPO_PATH ||= options.repoRoot;\n environment.MCP_UI_VIEWER = options.automation ? 'none' : (environment.MCP_UI_VIEWER ?? '');\n\n const personaPrompt = personaDirectory ? buildPersonaPrompt(personaRoot, personaDirectory) : undefined;\n let personaFile: string | undefined;\n if (personaPrompt) {\n personaFile = path.join(resources.temporaryDirectory, 'persona.md');\n await fs.promises.writeFile(personaFile, `${personaPrompt}\\n`, { mode: PRIVATE_FILE_MODE });\n }\n\n // One write, describing the child rather than this process. The state file\n // is the child's authority and the environment is what it projects for the\n // hooks and packages that can only read an environment.\n createHarnessSession(\n {\n root: options.repoRoot,\n majorMode: options.majorMode,\n temporaryDirectory: resources.temporaryDirectory,\n domains: options.domains,\n layers: selectedLayers,\n profile: profile?.name,\n profileIdentity: profile?.identity,\n profileVoice: profile?.voice,\n profileEnvironment,\n personaFile,\n skillDirectories: resources.skillDirectories,\n agentDirectories: resources.agentDirectories,\n additionalDirectories: options.additionalDirectories,\n childExtensions: [],\n pluginDirectories: options.pluginDirectories,\n pluginHooks: resources.pluginHooks,\n hookGroups,\n hooks: options.hooks,\n agents: options.agents,\n mcp: options.mcp,\n mcpConfigPath: resources.mcpConfigPath,\n mcpProjection: resources.mcpProjection,\n allowProtectedWrites: options.allowProtectedWrites,\n },\n { directory: resources.temporaryDirectory, environment },\n );\n if (options.agents) {\n environment.PI_SUBAGENT_EXTRA_AGENT_DIRS = resources.agentDirectories.join(path.delimiter);\n environment.PI_SUBAGENT_EXTRA_SKILL_DIRS = resources.skillDirectories.join(path.delimiter);\n }\n\n // Core hook groups load regardless; hookGroups selects the rest. The initial\n // state write includes an empty list deliberately: unset means every group.\n\n return {\n options,\n environment,\n resources,\n plugins,\n profile: profile?.name,\n personaDirectory,\n personaRoot,\n mcpAllowlist,\n majorModesConfig,\n selectedLayers,\n hookGroups,\n defaultThemePath,\n cleanup: resources.cleanup.bind(resources),\n };\n } catch (error) {\n // Preparation failed after the temp directory existed, so nothing will be\n // handed back to clean it up.\n await telemetry.recordError(HARNESS_EVENT.contextBuildFailed, error, {\n 'harness.phase': BUILD_CONTEXT_PHASE,\n 'harness.major_mode': options.majorMode,\n ...(profile?.name ? { 'harness.profile': profile.name } : {}),\n });\n await resources.cleanup();\n throw error;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"harnessContext.mjs","names":[],"sources":["../../../../src/builders/cli/harnessContext.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { globalPoConfigDirectory } from '@agimon-ai/po-config/config';\nimport { resolvePluginEntries, resolveSharedSkills } from '@agimon-ai/po-config/domains';\nimport type { DomainMcpAllowlist, PluginEntry } from '@agimon-ai/po-config/domains';\nimport {\n filterHookDisabledLayers,\n layerHookGroups,\n loadMajorModesConfig,\n resolveLayers,\n} from '@agimon-ai/po-config/majorModes';\nimport type { MajorModesConfig } from '@agimon-ai/po-config/majorModes';\nimport { applyProfileEnvironment, buildPersonaPrompt, resolveProfile } from '@agimon-ai/po-config/profiles';\nimport type { AgentProfile } from '@agimon-ai/po-config/profiles';\nimport { PO_MCP_SESSION_ENV_VAR } from '@agimon-ai/po-core/mcpSession';\nimport {\n createHarnessTelemetry,\n HARNESS_EVENT,\n type HarnessTelemetry,\n} from '@agimon-ai/po-core/runtimeLogSinkTelemetry';\nimport { resolveMcpAllowlist } from '@agimon-ai/po-domain/mcp';\nimport { materializePluginEntries } from '@agimon-ai/po-domain/plugins';\nimport { collectResources, type HarnessResources } from '@agimon-ai/po-domain/resources';\nimport { DEFAULT_THEME_NAME, writeDefaultTheme } from '@agimon-ai/po-ui/theme';\n\nimport { createHarnessSession } from '../../composition/harnessState';\nimport type { HarnessOptions } from '../../composition/types/harness';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst RESOLVE_CONFIG_PHASE = 'resolve_config';\nconst COLLECT_RESOURCES_PHASE = 'collect_resources';\nconst BUILD_CONTEXT_PHASE = 'build_context';\n\n/**\n * Everything a command needs, resolved once.\n *\n * All three commands need the same expensive preparation (domains resolved,\n * plugins collected into a temp directory, environment assembled), so it is\n * built here and handed to whichever command runs.\n */\nexport interface HarnessContext {\n options: HarnessOptions;\n environment: NodeJS.ProcessEnv;\n resources: HarnessResources;\n plugins: PluginEntry[];\n profile?: string;\n personaDirectory?: string;\n /** Root personaDirectory resolves against, when a profile supplied one. */\n personaRoot?: string;\n mcpAllowlist?: DomainMcpAllowlist;\n majorModesConfig: MajorModesConfig;\n selectedLayers: string[];\n hookGroups: string[];\n defaultThemePath: string;\n cleanup(): Promise<void>;\n}\n\n/**\n * Applies the provider environment a preset needs.\n *\n * Exported so a session server can apply the same rules before anything reads them.\n */\nexport function configurePreset(\n options: Pick<HarnessOptions, 'preset' | 'piArgs'>,\n environment: NodeJS.ProcessEnv,\n): void {\n if (options.preset === 'kimi') {\n if (!environment.KIMI_API_KEY && environment.KIMI_CODE_API_KEY) {\n environment.KIMI_API_KEY = environment.KIMI_CODE_API_KEY;\n }\n if (!environment.KIMI_API_KEY) {\n throw new Error('KIMI_API_KEY or KIMI_CODE_API_KEY is required for the kimi preset');\n }\n }\n}\n\n/** Resolves the optional persona and environment profile selected for this run. */\nexport function resolveHarnessProfile(options: HarnessOptions): AgentProfile | undefined {\n return options.profile ? resolveProfile(options.configRoot ?? options.repoRoot, options.profile) : undefined;\n}\n\n/**\n * Resolves the full matrix and stages every resource Pi will need.\n *\n * The caller owns the returned cleanup(): the temporary directory holding\n * skills, agents, and the generated MCP config has to outlive this function\n * and be removed once the selected command is done with it.\n */\nexport async function buildHarnessContext(\n options: HarnessOptions,\n telemetry: HarnessTelemetry = createHarnessTelemetry(),\n): Promise<HarnessContext> {\n // A worktree reads its workspace's configuration; resources still come from the checkout.\n const configRoot = options.configRoot ?? options.repoRoot;\n // Everything up to collectResources reads configuration off disk, so a single\n // malformed modes.yaml or profiles.yaml surfaces here as a failed launch\n // with no other record.\n const configured = await telemetry\n .runInSpan(`po_pi.${RESOLVE_CONFIG_PHASE}`, { 'harness.phase': RESOLVE_CONFIG_PHASE }, async () => {\n const majorModesConfig = loadMajorModesConfig(configRoot, options.homeDirectory, options.environment);\n const selectedLayers = filterHookDisabledLayers(\n majorModesConfig,\n resolveLayers(majorModesConfig, options.majorMode),\n options.hooks,\n );\n const profile = resolveHarnessProfile(options);\n if (!fs.existsSync(options.cwd) || !fs.statSync(options.cwd).isDirectory()) {\n throw new Error(`Working directory does not exist: ${options.cwd}`);\n }\n return {\n majorModesConfig,\n selectedLayers,\n hookGroups: layerHookGroups(majorModesConfig, selectedLayers),\n profile,\n plugins: await materializePluginEntries(\n resolvePluginEntries(configRoot, options.domains, options.pluginDirectories),\n ),\n mcpAllowlist: resolveMcpAllowlist(configRoot, options.domains),\n sharedSkills: resolveSharedSkills(configRoot, options.domains),\n };\n })\n .catch(async (error: unknown) => {\n await telemetry.recordError(HARNESS_EVENT.configLoadFailed, error, {\n 'harness.phase': RESOLVE_CONFIG_PHASE,\n 'harness.major_mode': options.majorMode,\n 'harness.domain_count': options.domains.length,\n });\n throw error;\n });\n\n const { majorModesConfig, selectedLayers, hookGroups, profile, plugins, mcpAllowlist } = configured;\n const personaDirectory = profile?.persona;\n // A global profile's persona lives beside the global config, not in the repo.\n const personaRoot = profile?.personaRoot ?? configRoot;\n const resources = await telemetry\n .runInSpan(\n `po_pi.${COLLECT_RESOURCES_PHASE}`,\n { 'harness.plugin.count': plugins.length, 'harness.agents': options.agents, 'harness.mcp': options.mcp },\n () =>\n collectResources(options.repoRoot, plugins, {\n agents: options.agents,\n mcp: options.mcp,\n // Set by a caller that stages into a directory it owns.\n temporaryDirectory: options.resourceDirectory,\n // Per repository, under the global home, so it outlives this process's resources.\n skillCacheDirectory:\n options.resourceDirectory === undefined\n ? path.join(\n globalPoConfigDirectory(options.homeDirectory ?? os.homedir()),\n 'cache',\n 'skills',\n crypto.createHash('sha256').update(path.resolve(options.repoRoot)).digest('hex').slice(0, 32),\n )\n : path.join(options.resourceDirectory, 'cache', 'skills'),\n mcpAllowlist,\n sharedSkills: configured.sharedSkills,\n pluginDataRoot: path.join(globalPoConfigDirectory(options.homeDirectory ?? os.homedir()), 'plugin-data'),\n }),\n )\n .catch(async (error: unknown) => {\n // Conflicting skills, agents, or MCP servers all fail here, and the plugin\n // that caused it is only identifiable from the message.\n await telemetry.recordError(HARNESS_EVENT.resourceCollectionFailed, error, {\n 'harness.phase': COLLECT_RESOURCES_PHASE,\n 'harness.plugin.count': plugins.length,\n });\n throw error;\n });\n\n try {\n const defaultThemePath = await writeDefaultTheme(resources.temporaryDirectory);\n const environment = { ...(options.environment ?? process.env) };\n // The file-backed projection published through Cordis is authoritative.\n // Never let a legacy parent-process projection leak into this Po session.\n delete environment[PO_MCP_SESSION_ENV_VAR];\n const profileEnvironment = applyProfileEnvironment(environment, profile?.env ?? {});\n\n configurePreset(options, environment);\n environment.NX_DAEMON ??= 'false';\n environment.PO_THEME ||= DEFAULT_THEME_NAME;\n environment.CLAUDE_PROJECT_DIR = options.repoRoot;\n environment.CODEX_REPO_ROOT = options.repoRoot;\n environment.ORIGINAL_REPO_PATH ||= options.repoRoot;\n environment.MCP_UI_VIEWER = options.automation ? 'none' : (environment.MCP_UI_VIEWER ?? '');\n\n const personaPrompt = personaDirectory ? buildPersonaPrompt(personaRoot, personaDirectory) : undefined;\n let personaFile: string | undefined;\n if (personaPrompt) {\n personaFile = path.join(resources.temporaryDirectory, 'persona.md');\n await fs.promises.writeFile(personaFile, `${personaPrompt}\\n`, { mode: PRIVATE_FILE_MODE });\n }\n\n // One write, describing the child rather than this process. The state file\n // is the child's authority and the environment is what it projects for the\n // hooks and packages that can only read an environment.\n createHarnessSession(\n {\n root: options.repoRoot,\n majorMode: options.majorMode,\n temporaryDirectory: resources.temporaryDirectory,\n domains: options.domains,\n layers: selectedLayers,\n profile: profile?.name,\n profileIdentity: profile?.identity,\n profileVoice: profile?.voice,\n profileEnvironment,\n personaFile,\n skillDirectories: resources.skillDirectories,\n agentDirectories: resources.agentDirectories,\n additionalDirectories: options.additionalDirectories,\n childExtensions: [],\n pluginDirectories: options.pluginDirectories,\n pluginHooks: resources.pluginHooks,\n hookGroups,\n hooks: options.hooks,\n agents: options.agents,\n mcp: options.mcp,\n mcpConfigPath: resources.mcpConfigPath,\n mcpProjection: resources.mcpProjection,\n allowProtectedWrites: options.allowProtectedWrites,\n },\n { directory: resources.temporaryDirectory, environment },\n );\n if (options.agents) {\n environment.PI_SUBAGENT_EXTRA_AGENT_DIRS = resources.agentDirectories.join(path.delimiter);\n environment.PI_SUBAGENT_EXTRA_SKILL_DIRS = resources.skillDirectories.join(path.delimiter);\n }\n\n // Core hook groups load regardless; hookGroups selects the rest. The initial\n // state write includes an empty list deliberately: unset means every group.\n\n return {\n options,\n environment,\n resources,\n plugins,\n profile: profile?.name,\n personaDirectory,\n personaRoot,\n mcpAllowlist,\n majorModesConfig,\n selectedLayers,\n hookGroups,\n defaultThemePath,\n cleanup: resources.cleanup.bind(resources),\n };\n } catch (error) {\n // Preparation failed after the temp directory existed, so nothing will be\n // handed back to clean it up.\n await telemetry.recordError(HARNESS_EVENT.contextBuildFailed, error, {\n 'harness.phase': BUILD_CONTEXT_PHASE,\n 'harness.major_mode': options.majorMode,\n ...(profile?.name ? { 'harness.profile': profile.name } : {}),\n });\n await resources.cleanup();\n throw error;\n }\n}\n"],"mappings":"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"}
@@ -54,7 +54,7 @@ function checkoutHookDescriptor(descriptor, sourceRoot, repoRoot, directory) {
54
54
  return { file };
55
55
  }
56
56
  async function hookParser(options) {
57
- const hasModuleRows = (0, _agimon_ai_po_config_layeredConfig.doomConfigCandidates)("hooks.yaml", options.repoRoot, options.homeDirectory).some((candidate) => {
57
+ const hasModuleRows = (0, _agimon_ai_po_config_layeredConfig.poConfigCandidates)("hooks.yaml", options.repoRoot, options.homeDirectory).some((candidate) => {
58
58
  if (!node_fs.default.existsSync(candidate.filePath)) return false;
59
59
  const document = (0, yaml.parse)(node_fs.default.readFileSync(candidate.filePath, "utf8"));
60
60
  if (!(0, _agimon_ai_po_core_runtimeJson.isRecord)(document) || !(0, _agimon_ai_po_core_runtimeJson.isRecord)(document.groups)) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["path","fs","isRecord","doomConfigCandidates","parseYaml","optionalPackageEntry","pathToFileURL","isBuiltin","crypto"],"sources":["../../../../src/builders/hooks/index.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport { isBuiltin } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { doomConfigCandidates } from '@agimon-ai/po-config/layeredConfig';\nimport type { MajorModesConfig } from '@agimon-ai/po-config/majorModes';\nimport { optionalPackageEntry } from '@agimon-ai/po-core/moduleResolution';\nimport { isRecord } from '@agimon-ai/po-core/runtimeJson';\nimport { parse as parseYaml } from 'yaml';\n\nexport const HOOK_MODULES_FILE = 'hook-modules.json';\n\n/** Add checkout source names while retaining the admitted generation's artifact mappings. */\nexport function checkoutHookDescriptor(\n descriptor: { file: string } | undefined,\n sourceRoot: string,\n repoRoot: string,\n directory: string,\n): { file: string } | undefined {\n if (!descriptor || path.resolve(sourceRoot) === path.resolve(repoRoot)) return descriptor;\n const value: unknown = JSON.parse(fs.readFileSync(descriptor.file, 'utf8'));\n if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.modules))\n throw new Error('invalid hook module descriptor');\n const sources = new Set<string>();\n const modules: Array<Record<string, unknown> & { source: string; artifact: string }> = [];\n // Validate all originals first. Never repair an invalid descriptor by dropping entries.\n for (const entry of value.modules) {\n if (\n !isRecord(entry) ||\n typeof entry.source !== 'string' ||\n typeof entry.artifact !== 'string' ||\n !path.isAbsolute(entry.source) ||\n !path.isAbsolute(entry.artifact) ||\n !['.mjs', '.js', '.cjs'].includes(path.extname(entry.artifact)) ||\n sources.has(entry.source)\n )\n throw new Error('invalid hook module artifact mapping');\n sources.add(entry.source);\n modules.push({ ...entry, source: entry.source, artifact: entry.artifact });\n }\n const aliases = [];\n for (const entry of modules) {\n const relative = path.relative(sourceRoot, entry.source);\n if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue;\n const source = path.resolve(repoRoot, relative);\n if (sources.has(source)) continue;\n sources.add(source);\n aliases.push({ ...entry, source });\n }\n if (aliases.length === 0) return descriptor;\n const file = path.join(directory, HOOK_MODULES_FILE);\n fs.writeFileSync(file, `${JSON.stringify({ ...value, modules: [...modules, ...aliases] }, null, 2)}\\n`, {\n mode: 0o600,\n });\n return { file };\n}\n\ninterface HookRegistryRow {\n module?: string;\n core: boolean;\n skipInSubagent?: boolean;\n registryId: string;\n groupId: string;\n rowId: string;\n event: string;\n}\n\n/** The optional hook package owns parsing; the CLI only consumes this capability. */\ninterface HookParserModule {\n createHookModules(options: { descriptor?: { file: string } }): {\n validate(rows: readonly HookRegistryRow[]): Promise<void>;\n dispose(): Promise<void>;\n };\n createHookDocumentReader(options: { homeDirectory: string; warn: (message: string) => void }): {\n registry(repoRoot: string): Promise<{\n entries: HookRegistryRow[];\n failure?: { message: string };\n }>;\n };\n}\n\n/** Core plus the union of every mode's selection. An unset selection admits all. */\nexport function requiredHookGroups(config: MajorModesConfig): Set<string> | undefined {\n const groups = new Set<string>();\n for (const mode of Object.values(config.majorMode)) {\n const layers = mode.layers.map((name) => config.layers[name]);\n if (layers.length === 0 || layers.some((layer) => layer?.hookGroups === undefined)) return undefined;\n for (const layer of layers) for (const group of layer.hookGroups ?? []) groups.add(group);\n }\n return groups;\n}\n\nasync function hookParser(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n}): Promise<HookParserModule | undefined> {\n const candidates = doomConfigCandidates('hooks.yaml', options.repoRoot, options.homeDirectory);\n // Command-only registries predate module compilation and do not require the package.\n // This is discovery only. The public parser still owns validation and attribution.\n const hasModuleRows = candidates.some((candidate) => {\n if (!fs.existsSync(candidate.filePath)) return false;\n const document: unknown = parseYaml(fs.readFileSync(candidate.filePath, 'utf8'));\n if (!isRecord(document) || !isRecord(document.groups)) return false;\n return Object.values(document.groups).some(\n (group) =>\n isRecord(group) &&\n Array.isArray(group.hooks) &&\n group.hooks.some((row: unknown) => isRecord(row) && isRecord(row.pi) && 'module' in row.pi),\n );\n });\n const parserEntry =\n optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.repoRoot, 'package.json')).href) ??\n (options.configRoot && options.configRoot !== options.repoRoot\n ? optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.configRoot, 'package.json')).href)\n : undefined) ??\n optionalPackageEntry('@agimon-ai/po-hook', import.meta.url);\n if (hasModuleRows && !parserEntry)\n throw new Error(\n 'Hook module rows require @agimon-ai/po-hook. Install it in the repository or select a layer that provides it, then run po sync.',\n );\n return hasModuleRows && parserEntry\n ? ((await import(pathToFileURL(parserEntry).href)) as HookParserModule)\n : undefined;\n}\n\n/** Validate the selected compiled hooks without importing repository code. */\nexport async function validateHookModules(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n hookGroups?: readonly string[];\n descriptor?: { file: string };\n isSubagent?: boolean;\n}): Promise<boolean> {\n const parser = await hookParser(options);\n if (!parser) return false;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const allowed = options.hookGroups === undefined ? undefined : new Set(options.hookGroups);\n const rows = read.entries\n .filter((row) => row.core || !allowed || allowed.has(row.groupId))\n .filter((row) => !(options.isSubagent && row.skipInSubagent));\n const modules = parser.createHookModules({ descriptor: options.descriptor });\n try {\n await modules.validate(rows);\n return rows.some((row) => row.module !== undefined);\n } finally {\n await modules.dispose();\n }\n}\n\n/** Built JavaScript loads in place or from a package snapshot; TypeScript is compiled first. */\nconst BUILT_EXTENSIONS = new Set(['.mjs', '.js', '.cjs']);\nconst TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);\n/** Pi's shared runtime must resolve to the copy this harness runs, as the sync compiler pinned it. */\nconst HOSTED_PACKAGES = new Set(['@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', '@earendil-works/pi-ai']);\n\n/**\n * Compiles a TypeScript hook into one ESM file named by the hash of its bytes.\n *\n * Relative imports are bundled, so editing any of them yields a new file and URL. Bare\n * imports are left external as the absolute file their importer resolves, or Pi's copy\n * for hosted packages, so the artifact's location does not change what they load. A bare\n * import that resolves to TypeScript is bundled, because Node does not strip it under\n * node_modules. `import.meta.url` keeps naming each original source file.\n */\nasync function compileHookSource(source: string, directory: string): Promise<string> {\n const { rolldown } = await import('rolldown');\n const build = await rolldown({\n input: source,\n platform: 'node',\n onLog(_level, log, handler) {\n if (log.code === 'UNRESOLVED_IMPORT') handler('error', log);\n },\n plugins: [\n {\n name: 'po-hook-externals',\n async resolveId(specifier, importer, extra) {\n if (!importer || specifier.startsWith('.') || path.isAbsolute(specifier)) return null;\n if (isBuiltin(specifier)) return { id: specifier, external: true };\n const root = specifier.split('/', specifier.startsWith('@') ? 2 : 1).join('/');\n const hosted = HOSTED_PACKAGES.has(root) ? optionalPackageEntry(specifier, import.meta.url) : undefined;\n const resolved = hosted ?? (await this.resolve(specifier, importer, { ...extra, skipSelf: true }))?.id;\n if (resolved === undefined || !path.isAbsolute(resolved)) return null;\n if (TYPESCRIPT_EXTENSIONS.has(path.extname(resolved))) return resolved;\n return { id: pathToFileURL(resolved).href, external: true };\n },\n transform(code, id) {\n if (!path.isAbsolute(id) || !code.includes('import.meta.url')) return null;\n return code.replaceAll(/\\bimport\\.meta\\.url\\b/gu, JSON.stringify(pathToFileURL(id).href));\n },\n },\n ],\n });\n let code: string;\n try {\n const generated = await build.generate({ format: 'esm', sourcemap: false, codeSplitting: false });\n code = generated.output[0].code;\n } finally {\n await build.close();\n }\n await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });\n const file = path.join(directory, `${crypto.createHash('sha256').update(code).digest('hex')}.mjs`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, code, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return file;\n}\n\n/**\n * Writes a hook module descriptor from the live registry.\n *\n * A built JavaScript source maps through `artifactFor` (the server's content-addressed\n * package paths); a TypeScript source is compiled into `directory`. A source that cannot\n * be prepared is left out with a notice naming its rows, so validation fails closed for\n * them. The descriptor is named by its content, so equal registries share one file.\n * Returns undefined when no registry row names a module.\n */\nexport async function writeLiveHookModules(options: {\n repoRoot: string;\n homeDirectory: string;\n directory: string;\n artifactFor: (source: string) => Promise<string>;\n onNotice?: (message: string) => void;\n}): Promise<{ file: string } | undefined> {\n const parser = await hookParser(options);\n if (!parser) return undefined;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const sources = new Map<string, Array<{ registry: string; groupId: string; rowId: string; event: string }>>();\n for (const row of read.entries) {\n if (!row.module) continue;\n const rows = sources.get(row.module) ?? [];\n rows.push({ registry: row.registryId, groupId: row.groupId, rowId: row.rowId, event: row.event });\n sources.set(row.module, rows);\n }\n if (sources.size === 0) return undefined;\n const modules = [];\n for (const [source, rows] of sources) {\n const extension = path.extname(source);\n try {\n let artifact: string;\n if (TYPESCRIPT_EXTENSIONS.has(extension)) artifact = await compileHookSource(source, options.directory);\n else if (BUILT_EXTENSIONS.has(extension)) artifact = await options.artifactFor(source);\n else throw new Error('a hook module must be TypeScript (.ts, .mts, .cts) or JavaScript (.mjs, .js, .cjs).');\n modules.push({ source, artifact, rows });\n } catch (error) {\n // Left out of the descriptor, so validation fails closed for its rows.\n const attribution = rows\n .map((row) => `${row.registry}: group ${row.groupId}, row ${row.rowId}, event ${row.event}`)\n .join('; ');\n options.onNotice?.(\n `Hook module ${source} is unavailable (${attribution}): ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n const content = `${JSON.stringify({ version: 1, modules }, null, 2)}\\n`;\n await fs.promises.mkdir(options.directory, { recursive: true, mode: 0o700 });\n const file = path.join(options.directory, `${crypto.createHash('sha256').update(content).digest('hex')}.json`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, content, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return { file };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["path","fs","isRecord","poConfigCandidates","parseYaml","optionalPackageEntry","pathToFileURL","isBuiltin","crypto"],"sources":["../../../../src/builders/hooks/index.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport { isBuiltin } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { poConfigCandidates } from '@agimon-ai/po-config/layeredConfig';\nimport type { MajorModesConfig } from '@agimon-ai/po-config/majorModes';\nimport { optionalPackageEntry } from '@agimon-ai/po-core/moduleResolution';\nimport { isRecord } from '@agimon-ai/po-core/runtimeJson';\nimport { parse as parseYaml } from 'yaml';\n\nexport const HOOK_MODULES_FILE = 'hook-modules.json';\n\n/** Add checkout source names while retaining the admitted generation's artifact mappings. */\nexport function checkoutHookDescriptor(\n descriptor: { file: string } | undefined,\n sourceRoot: string,\n repoRoot: string,\n directory: string,\n): { file: string } | undefined {\n if (!descriptor || path.resolve(sourceRoot) === path.resolve(repoRoot)) return descriptor;\n const value: unknown = JSON.parse(fs.readFileSync(descriptor.file, 'utf8'));\n if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.modules))\n throw new Error('invalid hook module descriptor');\n const sources = new Set<string>();\n const modules: Array<Record<string, unknown> & { source: string; artifact: string }> = [];\n // Validate all originals first. Never repair an invalid descriptor by dropping entries.\n for (const entry of value.modules) {\n if (\n !isRecord(entry) ||\n typeof entry.source !== 'string' ||\n typeof entry.artifact !== 'string' ||\n !path.isAbsolute(entry.source) ||\n !path.isAbsolute(entry.artifact) ||\n !['.mjs', '.js', '.cjs'].includes(path.extname(entry.artifact)) ||\n sources.has(entry.source)\n )\n throw new Error('invalid hook module artifact mapping');\n sources.add(entry.source);\n modules.push({ ...entry, source: entry.source, artifact: entry.artifact });\n }\n const aliases = [];\n for (const entry of modules) {\n const relative = path.relative(sourceRoot, entry.source);\n if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue;\n const source = path.resolve(repoRoot, relative);\n if (sources.has(source)) continue;\n sources.add(source);\n aliases.push({ ...entry, source });\n }\n if (aliases.length === 0) return descriptor;\n const file = path.join(directory, HOOK_MODULES_FILE);\n fs.writeFileSync(file, `${JSON.stringify({ ...value, modules: [...modules, ...aliases] }, null, 2)}\\n`, {\n mode: 0o600,\n });\n return { file };\n}\n\ninterface HookRegistryRow {\n module?: string;\n core: boolean;\n skipInSubagent?: boolean;\n registryId: string;\n groupId: string;\n rowId: string;\n event: string;\n}\n\n/** The optional hook package owns parsing; the CLI only consumes this capability. */\ninterface HookParserModule {\n createHookModules(options: { descriptor?: { file: string } }): {\n validate(rows: readonly HookRegistryRow[]): Promise<void>;\n dispose(): Promise<void>;\n };\n createHookDocumentReader(options: { homeDirectory: string; warn: (message: string) => void }): {\n registry(repoRoot: string): Promise<{\n entries: HookRegistryRow[];\n failure?: { message: string };\n }>;\n };\n}\n\n/** Core plus the union of every mode's selection. An unset selection admits all. */\nexport function requiredHookGroups(config: MajorModesConfig): Set<string> | undefined {\n const groups = new Set<string>();\n for (const mode of Object.values(config.majorMode)) {\n const layers = mode.layers.map((name) => config.layers[name]);\n if (layers.length === 0 || layers.some((layer) => layer?.hookGroups === undefined)) return undefined;\n for (const layer of layers) for (const group of layer.hookGroups ?? []) groups.add(group);\n }\n return groups;\n}\n\nasync function hookParser(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n}): Promise<HookParserModule | undefined> {\n const candidates = poConfigCandidates('hooks.yaml', options.repoRoot, options.homeDirectory);\n // Command-only registries predate module compilation and do not require the package.\n // This is discovery only. The public parser still owns validation and attribution.\n const hasModuleRows = candidates.some((candidate) => {\n if (!fs.existsSync(candidate.filePath)) return false;\n const document: unknown = parseYaml(fs.readFileSync(candidate.filePath, 'utf8'));\n if (!isRecord(document) || !isRecord(document.groups)) return false;\n return Object.values(document.groups).some(\n (group) =>\n isRecord(group) &&\n Array.isArray(group.hooks) &&\n group.hooks.some((row: unknown) => isRecord(row) && isRecord(row.pi) && 'module' in row.pi),\n );\n });\n const parserEntry =\n optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.repoRoot, 'package.json')).href) ??\n (options.configRoot && options.configRoot !== options.repoRoot\n ? optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.configRoot, 'package.json')).href)\n : undefined) ??\n optionalPackageEntry('@agimon-ai/po-hook', import.meta.url);\n if (hasModuleRows && !parserEntry)\n throw new Error(\n 'Hook module rows require @agimon-ai/po-hook. Install it in the repository or select a layer that provides it, then run po sync.',\n );\n return hasModuleRows && parserEntry\n ? ((await import(pathToFileURL(parserEntry).href)) as HookParserModule)\n : undefined;\n}\n\n/** Validate the selected compiled hooks without importing repository code. */\nexport async function validateHookModules(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n hookGroups?: readonly string[];\n descriptor?: { file: string };\n isSubagent?: boolean;\n}): Promise<boolean> {\n const parser = await hookParser(options);\n if (!parser) return false;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const allowed = options.hookGroups === undefined ? undefined : new Set(options.hookGroups);\n const rows = read.entries\n .filter((row) => row.core || !allowed || allowed.has(row.groupId))\n .filter((row) => !(options.isSubagent && row.skipInSubagent));\n const modules = parser.createHookModules({ descriptor: options.descriptor });\n try {\n await modules.validate(rows);\n return rows.some((row) => row.module !== undefined);\n } finally {\n await modules.dispose();\n }\n}\n\n/** Built JavaScript loads in place or from a package snapshot; TypeScript is compiled first. */\nconst BUILT_EXTENSIONS = new Set(['.mjs', '.js', '.cjs']);\nconst TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);\n/** Pi's shared runtime must resolve to the copy this harness runs, as the sync compiler pinned it. */\nconst HOSTED_PACKAGES = new Set(['@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', '@earendil-works/pi-ai']);\n\n/**\n * Compiles a TypeScript hook into one ESM file named by the hash of its bytes.\n *\n * Relative imports are bundled, so editing any of them yields a new file and URL. Bare\n * imports are left external as the absolute file their importer resolves, or Pi's copy\n * for hosted packages, so the artifact's location does not change what they load. A bare\n * import that resolves to TypeScript is bundled, because Node does not strip it under\n * node_modules. `import.meta.url` keeps naming each original source file.\n */\nasync function compileHookSource(source: string, directory: string): Promise<string> {\n const { rolldown } = await import('rolldown');\n const build = await rolldown({\n input: source,\n platform: 'node',\n onLog(_level, log, handler) {\n if (log.code === 'UNRESOLVED_IMPORT') handler('error', log);\n },\n plugins: [\n {\n name: 'po-hook-externals',\n async resolveId(specifier, importer, extra) {\n if (!importer || specifier.startsWith('.') || path.isAbsolute(specifier)) return null;\n if (isBuiltin(specifier)) return { id: specifier, external: true };\n const root = specifier.split('/', specifier.startsWith('@') ? 2 : 1).join('/');\n const hosted = HOSTED_PACKAGES.has(root) ? optionalPackageEntry(specifier, import.meta.url) : undefined;\n const resolved = hosted ?? (await this.resolve(specifier, importer, { ...extra, skipSelf: true }))?.id;\n if (resolved === undefined || !path.isAbsolute(resolved)) return null;\n if (TYPESCRIPT_EXTENSIONS.has(path.extname(resolved))) return resolved;\n return { id: pathToFileURL(resolved).href, external: true };\n },\n transform(code, id) {\n if (!path.isAbsolute(id) || !code.includes('import.meta.url')) return null;\n return code.replaceAll(/\\bimport\\.meta\\.url\\b/gu, JSON.stringify(pathToFileURL(id).href));\n },\n },\n ],\n });\n let code: string;\n try {\n const generated = await build.generate({ format: 'esm', sourcemap: false, codeSplitting: false });\n code = generated.output[0].code;\n } finally {\n await build.close();\n }\n await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });\n const file = path.join(directory, `${crypto.createHash('sha256').update(code).digest('hex')}.mjs`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, code, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return file;\n}\n\n/**\n * Writes a hook module descriptor from the live registry.\n *\n * A built JavaScript source maps through `artifactFor` (the server's content-addressed\n * package paths); a TypeScript source is compiled into `directory`. A source that cannot\n * be prepared is left out with a notice naming its rows, so validation fails closed for\n * them. The descriptor is named by its content, so equal registries share one file.\n * Returns undefined when no registry row names a module.\n */\nexport async function writeLiveHookModules(options: {\n repoRoot: string;\n homeDirectory: string;\n directory: string;\n artifactFor: (source: string) => Promise<string>;\n onNotice?: (message: string) => void;\n}): Promise<{ file: string } | undefined> {\n const parser = await hookParser(options);\n if (!parser) return undefined;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const sources = new Map<string, Array<{ registry: string; groupId: string; rowId: string; event: string }>>();\n for (const row of read.entries) {\n if (!row.module) continue;\n const rows = sources.get(row.module) ?? [];\n rows.push({ registry: row.registryId, groupId: row.groupId, rowId: row.rowId, event: row.event });\n sources.set(row.module, rows);\n }\n if (sources.size === 0) return undefined;\n const modules = [];\n for (const [source, rows] of sources) {\n const extension = path.extname(source);\n try {\n let artifact: string;\n if (TYPESCRIPT_EXTENSIONS.has(extension)) artifact = await compileHookSource(source, options.directory);\n else if (BUILT_EXTENSIONS.has(extension)) artifact = await options.artifactFor(source);\n else throw new Error('a hook module must be TypeScript (.ts, .mts, .cts) or JavaScript (.mjs, .js, .cjs).');\n modules.push({ source, artifact, rows });\n } catch (error) {\n // Left out of the descriptor, so validation fails closed for its rows.\n const attribution = rows\n .map((row) => `${row.registry}: group ${row.groupId}, row ${row.rowId}, event ${row.event}`)\n .join('; ');\n options.onNotice?.(\n `Hook module ${source} is unavailable (${attribution}): ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n const content = `${JSON.stringify({ version: 1, modules }, null, 2)}\\n`;\n await fs.promises.mkdir(options.directory, { recursive: true, mode: 0o700 });\n const file = path.join(options.directory, `${crypto.createHash('sha256').update(content).digest('hex')}.json`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, content, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return { file };\n}\n"],"mappings":";;;;;;;;;;;;;;AAYA,MAAa,oBAAoB;;AAGjC,SAAgB,uBACd,YACA,YACA,UACA,WAC8B;CAC9B,IAAI,CAAC,cAAcA,UAAAA,QAAK,QAAQ,UAAU,MAAMA,UAAAA,QAAK,QAAQ,QAAQ,GAAG,OAAO;CAC/E,MAAM,QAAiB,KAAK,MAAMC,QAAAA,QAAG,aAAa,WAAW,MAAM,MAAM,CAAC;CAC1E,IAAI,EAAA,GAACC,+BAAAA,SAAAA,CAAS,KAAK,KAAK,MAAM,YAAY,KAAK,CAAC,MAAM,QAAQ,MAAM,OAAO,GACzE,MAAM,IAAI,MAAM,gCAAgC;CAClD,MAAM,0BAAU,IAAI,IAAY;CAChC,MAAM,UAAiF,CAAC;CAExF,KAAK,MAAM,SAAS,MAAM,SAAS;EACjC,IACE,EAAA,GAACA,+BAAAA,SAAAA,CAAS,KAAK,KACf,OAAO,MAAM,WAAW,YACxB,OAAO,MAAM,aAAa,YAC1B,CAACF,UAAAA,QAAK,WAAW,MAAM,MAAM,KAC7B,CAACA,UAAAA,QAAK,WAAW,MAAM,QAAQ,KAC/B,CAAC;GAAC;GAAQ;GAAO;EAAM,CAAC,CAAC,SAASA,UAAAA,QAAK,QAAQ,MAAM,QAAQ,CAAC,KAC9D,QAAQ,IAAI,MAAM,MAAM,GAExB,MAAM,IAAI,MAAM,sCAAsC;EACxD,QAAQ,IAAI,MAAM,MAAM;EACxB,QAAQ,KAAK;GAAE,GAAG;GAAO,QAAQ,MAAM;GAAQ,UAAU,MAAM;EAAS,CAAC;CAC3E;CACA,MAAM,UAAU,CAAC;CACjB,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAWA,UAAAA,QAAK,SAAS,YAAY,MAAM,MAAM;EACvD,IAAI,CAAC,YAAY,aAAa,QAAQ,SAAS,WAAW,KAAKA,UAAAA,QAAK,KAAK,KAAKA,UAAAA,QAAK,WAAW,QAAQ,GAAG;EACzG,MAAM,SAASA,UAAAA,QAAK,QAAQ,UAAU,QAAQ;EAC9C,IAAI,QAAQ,IAAI,MAAM,GAAG;EACzB,QAAQ,IAAI,MAAM;EAClB,QAAQ,KAAK;GAAE,GAAG;GAAO;EAAO,CAAC;CACnC;CACA,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,MAAM,OAAOA,UAAAA,QAAK,KAAK,WAAW,iBAAiB;CACnD,QAAA,QAAG,cAAc,MAAM,GAAG,KAAK,UAAU;EAAE,GAAG;EAAO,SAAS,CAAC,GAAG,SAAS,GAAG,OAAO;CAAE,GAAG,MAAM,CAAC,EAAE,KAAK,EACtG,MAAM,IACR,CAAC;CACD,OAAO,EAAE,KAAK;AAChB;AAqCA,eAAe,WAAW,SAIgB;CAIxC,MAAM,iBAAA,GAHaG,mCAAAA,mBAAAA,CAAmB,cAAc,QAAQ,UAAU,QAAQ,aAG/C,CAAC,CAAC,MAAM,cAAc;EACnD,IAAI,CAACF,QAAAA,QAAG,WAAW,UAAU,QAAQ,GAAG,OAAO;EAC/C,MAAM,YAAA,GAAoBG,KAAAA,MAAAA,CAAUH,QAAAA,QAAG,aAAa,UAAU,UAAU,MAAM,CAAC;EAC/E,IAAI,EAAA,GAACC,+BAAAA,SAAAA,CAAS,QAAQ,KAAK,EAAA,GAACA,+BAAAA,SAAAA,CAAS,SAAS,MAAM,GAAG,OAAO;EAC9D,OAAO,OAAO,OAAO,SAAS,MAAM,CAAC,CAAC,MACnC,WAAA,GACCA,+BAAAA,SAAAA,CAAS,KAAK,KACd,MAAM,QAAQ,MAAM,KAAK,KACzB,MAAM,MAAM,MAAM,SAAA,GAAiBA,+BAAAA,SAAAA,CAAS,GAAG,MAAA,GAAKA,+BAAAA,SAAAA,CAAS,IAAI,EAAE,KAAK,YAAY,IAAI,EAAE,CAC9F;CACF,CAAC;CACD,MAAM,eAAA,GACJG,oCAAAA,qBAAAA,CAAqB,uBAAA,GAAsBC,SAAAA,cAAAA,CAAcN,UAAAA,QAAK,KAAK,QAAQ,UAAU,cAAc,CAAC,CAAC,CAAC,IAAI,MACzG,QAAQ,cAAc,QAAQ,eAAe,QAAQ,YAAA,GAClDK,oCAAAA,qBAAAA,CAAqB,uBAAA,GAAsBC,SAAAA,cAAAA,CAAcN,UAAAA,QAAK,KAAK,QAAQ,YAAY,cAAc,CAAC,CAAC,CAAC,IAAI,IAC5G,KAAA,OAAA,GACJK,oCAAAA,qBAAAA,CAAqB,sBAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAAqC;CAC5D,IAAI,iBAAiB,CAAC,aACpB,MAAM,IAAI,MACR,iIACF;CACF,OAAO,iBAAiB,cAClB,MAAM,QAAA,GAAOC,SAAAA,cAAAA,CAAc,WAAW,CAAC,CAAC,QAC1C,KAAA;AACN;;AAGA,eAAsB,oBAAoB,SAOrB;CACnB,MAAM,SAAS,MAAM,WAAW,OAAO;CACvC,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,OAAO,MAAM,OAChB,yBAAyB;EAAE,eAAe,QAAQ;EAAe,YAAY,KAAA;CAAU,CAAC,CAAC,CACzF,SAAS,QAAQ,QAAQ;CAC5B,IAAI,KAAK,SAAS,MAAM,IAAI,MAAM,KAAK,QAAQ,OAAO;CACtD,MAAM,UAAU,QAAQ,eAAe,KAAA,IAAY,KAAA,IAAY,IAAI,IAAI,QAAQ,UAAU;CACzF,MAAM,OAAO,KAAK,QACf,QAAQ,QAAQ,IAAI,QAAQ,CAAC,WAAW,QAAQ,IAAI,IAAI,OAAO,CAAC,CAAC,CACjE,QAAQ,QAAQ,EAAE,QAAQ,cAAc,IAAI,eAAe;CAC9D,MAAM,UAAU,OAAO,kBAAkB,EAAE,YAAY,QAAQ,WAAW,CAAC;CAC3E,IAAI;EACF,MAAM,QAAQ,SAAS,IAAI;EAC3B,OAAO,KAAK,MAAM,QAAQ,IAAI,WAAW,KAAA,CAAS;CACpD,UAAU;EACR,MAAM,QAAQ,QAAQ;CACxB;AACF;;AAGA,MAAM,mCAAmB,IAAI,IAAI;CAAC;CAAQ;CAAO;AAAM,CAAC;AACxD,MAAM,wCAAwB,IAAI,IAAI;CAAC;CAAO;CAAQ;AAAM,CAAC;;AAE7D,MAAM,kCAAkB,IAAI,IAAI;CAAC;CAAmC;CAA0B;AAAuB,CAAC;;;;;;;;;;AAWtH,eAAe,kBAAkB,QAAgB,WAAoC;CACnF,MAAM,EAAE,aAAa,MAAM,OAAO;CAClC,MAAM,QAAQ,MAAM,SAAS;EAC3B,OAAO;EACP,UAAU;EACV,MAAM,QAAQ,KAAK,SAAS;GAC1B,IAAI,IAAI,SAAS,qBAAqB,QAAQ,SAAS,GAAG;EAC5D;EACA,SAAS,CACP;GACE,MAAM;GACN,MAAM,UAAU,WAAW,UAAU,OAAO;IAC1C,IAAI,CAAC,YAAY,UAAU,WAAW,GAAG,KAAKN,UAAAA,QAAK,WAAW,SAAS,GAAG,OAAO;IACjF,KAAA,GAAIO,YAAAA,UAAAA,CAAU,SAAS,GAAG,OAAO;KAAE,IAAI;KAAW,UAAU;IAAK;IACjE,MAAM,OAAO,UAAU,MAAM,KAAK,UAAU,WAAW,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG;IAE7E,MAAM,YADS,gBAAgB,IAAI,IAAI,KAAA,GAAIF,oCAAAA,qBAAAA,CAAqB,WAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA0B,IAAI,KAAA,OAClE,MAAM,KAAK,QAAQ,WAAW,UAAU;KAAE,GAAG;KAAO,UAAU;IAAK,CAAC,EAAA,EAAI;IACpG,IAAI,aAAa,KAAA,KAAa,CAACL,UAAAA,QAAK,WAAW,QAAQ,GAAG,OAAO;IACjE,IAAI,sBAAsB,IAAIA,UAAAA,QAAK,QAAQ,QAAQ,CAAC,GAAG,OAAO;IAC9D,OAAO;KAAE,KAAA,GAAIM,SAAAA,cAAAA,CAAc,QAAQ,CAAC,CAAC;KAAM,UAAU;IAAK;GAC5D;GACA,UAAU,MAAM,IAAI;IAClB,IAAI,CAACN,UAAAA,QAAK,WAAW,EAAE,KAAK,CAAC,KAAK,SAAS,iBAAiB,GAAG,OAAO;IACtE,OAAO,KAAK,WAAW,2BAA2B,KAAK,WAAA,GAAUM,SAAAA,cAAAA,CAAc,EAAE,CAAC,CAAC,IAAI,CAAC;GAC1F;EACF,CACF;CACF,CAAC;CACD,IAAI;CACJ,IAAI;EAEF,QAAO,MADiB,MAAM,SAAS;GAAE,QAAQ;GAAO,WAAW;GAAO,eAAe;EAAM,CAAC,EAAA,CAC/E,OAAO,EAAE,CAAC;CAC7B,UAAU;EACR,MAAM,MAAM,MAAM;CACpB;CACA,MAAML,QAAAA,QAAG,SAAS,MAAM,WAAW;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CACnE,MAAM,OAAOD,UAAAA,QAAK,KAAK,WAAW,GAAGQ,YAAAA,QAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE,KAAK;CACjG,IAAI,CAACP,QAAAA,QAAG,WAAW,IAAI,GAAG;EACxB,MAAM,UAAU,GAAG,KAAK,GAAG,OAAO,QAAQ,GAAG,EAAE,GAAGO,YAAAA,QAAO,WAAW;EACpE,MAAMP,QAAAA,QAAG,SAAS,UAAU,SAAS,MAAM,EAAE,MAAM,IAAM,CAAC;EAC1D,MAAMA,QAAAA,QAAG,SAAS,OAAO,SAAS,IAAI;CACxC;CACA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,qBAAqB,SAMD;CACxC,MAAM,SAAS,MAAM,WAAW,OAAO;CACvC,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,OAAO,MAAM,OAChB,yBAAyB;EAAE,eAAe,QAAQ;EAAe,YAAY,KAAA;CAAU,CAAC,CAAC,CACzF,SAAS,QAAQ,QAAQ;CAC5B,IAAI,KAAK,SAAS,MAAM,IAAI,MAAM,KAAK,QAAQ,OAAO;CACtD,MAAM,0BAAU,IAAI,IAAwF;CAC5G,KAAK,MAAM,OAAO,KAAK,SAAS;EAC9B,IAAI,CAAC,IAAI,QAAQ;EACjB,MAAM,OAAO,QAAQ,IAAI,IAAI,MAAM,KAAK,CAAC;EACzC,KAAK,KAAK;GAAE,UAAU,IAAI;GAAY,SAAS,IAAI;GAAS,OAAO,IAAI;GAAO,OAAO,IAAI;EAAM,CAAC;EAChG,QAAQ,IAAI,IAAI,QAAQ,IAAI;CAC9B;CACA,IAAI,QAAQ,SAAS,GAAG,OAAO,KAAA;CAC/B,MAAM,UAAU,CAAC;CACjB,KAAK,MAAM,CAAC,QAAQ,SAAS,SAAS;EACpC,MAAM,YAAYD,UAAAA,QAAK,QAAQ,MAAM;EACrC,IAAI;GACF,IAAI;GACJ,IAAI,sBAAsB,IAAI,SAAS,GAAG,WAAW,MAAM,kBAAkB,QAAQ,QAAQ,SAAS;QACjG,IAAI,iBAAiB,IAAI,SAAS,GAAG,WAAW,MAAM,QAAQ,YAAY,MAAM;QAChF,MAAM,IAAI,MAAM,qFAAqF;GAC1G,QAAQ,KAAK;IAAE;IAAQ;IAAU;GAAK,CAAC;EACzC,SAAS,OAAO;GAEd,MAAM,cAAc,KACjB,KAAK,QAAQ,GAAG,IAAI,SAAS,UAAU,IAAI,QAAQ,QAAQ,IAAI,MAAM,UAAU,IAAI,OAAO,CAAC,CAC3F,KAAK,IAAI;GACZ,QAAQ,WACN,eAAe,OAAO,mBAAmB,YAAY,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACjH;EACF;CACF;CACA,MAAM,UAAU,GAAG,KAAK,UAAU;EAAE,SAAS;EAAG;CAAQ,GAAG,MAAM,CAAC,EAAE;CACpE,MAAMC,QAAAA,QAAG,SAAS,MAAM,QAAQ,WAAW;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CAC3E,MAAM,OAAOD,UAAAA,QAAK,KAAK,QAAQ,WAAW,GAAGQ,YAAAA,QAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,OAAO,KAAK,EAAE,MAAM;CAC7G,IAAI,CAACP,QAAAA,QAAG,WAAW,IAAI,GAAG;EACxB,MAAM,UAAU,GAAG,KAAK,GAAG,OAAO,QAAQ,GAAG,EAAE,GAAGO,YAAAA,QAAO,WAAW;EACpE,MAAMP,QAAAA,QAAG,SAAS,UAAU,SAAS,SAAS,EAAE,MAAM,IAAM,CAAC;EAC7D,MAAMA,QAAAA,QAAG,SAAS,OAAO,SAAS,IAAI;CACxC;CACA,OAAO,EAAE,KAAK;AAChB"}
@@ -5,7 +5,7 @@ import { isRecord } from "@agimon-ai/po-core/runtimeJson";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { optionalPackageEntry } from "@agimon-ai/po-core/moduleResolution";
7
7
  import crypto from "node:crypto";
8
- import { doomConfigCandidates } from "@agimon-ai/po-config/layeredConfig";
8
+ import { poConfigCandidates } from "@agimon-ai/po-config/layeredConfig";
9
9
  import { parse } from "yaml";
10
10
  //#region src/builders/hooks/index.ts
11
11
  const HOOK_MODULES_FILE = "hook-modules.json";
@@ -50,7 +50,7 @@ function checkoutHookDescriptor(descriptor, sourceRoot, repoRoot, directory) {
50
50
  return { file };
51
51
  }
52
52
  async function hookParser(options) {
53
- const hasModuleRows = doomConfigCandidates("hooks.yaml", options.repoRoot, options.homeDirectory).some((candidate) => {
53
+ const hasModuleRows = poConfigCandidates("hooks.yaml", options.repoRoot, options.homeDirectory).some((candidate) => {
54
54
  if (!fs.existsSync(candidate.filePath)) return false;
55
55
  const document = parse(fs.readFileSync(candidate.filePath, "utf8"));
56
56
  if (!isRecord(document) || !isRecord(document.groups)) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["parseYaml"],"sources":["../../../../src/builders/hooks/index.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport { isBuiltin } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { doomConfigCandidates } from '@agimon-ai/po-config/layeredConfig';\nimport type { MajorModesConfig } from '@agimon-ai/po-config/majorModes';\nimport { optionalPackageEntry } from '@agimon-ai/po-core/moduleResolution';\nimport { isRecord } from '@agimon-ai/po-core/runtimeJson';\nimport { parse as parseYaml } from 'yaml';\n\nexport const HOOK_MODULES_FILE = 'hook-modules.json';\n\n/** Add checkout source names while retaining the admitted generation's artifact mappings. */\nexport function checkoutHookDescriptor(\n descriptor: { file: string } | undefined,\n sourceRoot: string,\n repoRoot: string,\n directory: string,\n): { file: string } | undefined {\n if (!descriptor || path.resolve(sourceRoot) === path.resolve(repoRoot)) return descriptor;\n const value: unknown = JSON.parse(fs.readFileSync(descriptor.file, 'utf8'));\n if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.modules))\n throw new Error('invalid hook module descriptor');\n const sources = new Set<string>();\n const modules: Array<Record<string, unknown> & { source: string; artifact: string }> = [];\n // Validate all originals first. Never repair an invalid descriptor by dropping entries.\n for (const entry of value.modules) {\n if (\n !isRecord(entry) ||\n typeof entry.source !== 'string' ||\n typeof entry.artifact !== 'string' ||\n !path.isAbsolute(entry.source) ||\n !path.isAbsolute(entry.artifact) ||\n !['.mjs', '.js', '.cjs'].includes(path.extname(entry.artifact)) ||\n sources.has(entry.source)\n )\n throw new Error('invalid hook module artifact mapping');\n sources.add(entry.source);\n modules.push({ ...entry, source: entry.source, artifact: entry.artifact });\n }\n const aliases = [];\n for (const entry of modules) {\n const relative = path.relative(sourceRoot, entry.source);\n if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue;\n const source = path.resolve(repoRoot, relative);\n if (sources.has(source)) continue;\n sources.add(source);\n aliases.push({ ...entry, source });\n }\n if (aliases.length === 0) return descriptor;\n const file = path.join(directory, HOOK_MODULES_FILE);\n fs.writeFileSync(file, `${JSON.stringify({ ...value, modules: [...modules, ...aliases] }, null, 2)}\\n`, {\n mode: 0o600,\n });\n return { file };\n}\n\ninterface HookRegistryRow {\n module?: string;\n core: boolean;\n skipInSubagent?: boolean;\n registryId: string;\n groupId: string;\n rowId: string;\n event: string;\n}\n\n/** The optional hook package owns parsing; the CLI only consumes this capability. */\ninterface HookParserModule {\n createHookModules(options: { descriptor?: { file: string } }): {\n validate(rows: readonly HookRegistryRow[]): Promise<void>;\n dispose(): Promise<void>;\n };\n createHookDocumentReader(options: { homeDirectory: string; warn: (message: string) => void }): {\n registry(repoRoot: string): Promise<{\n entries: HookRegistryRow[];\n failure?: { message: string };\n }>;\n };\n}\n\n/** Core plus the union of every mode's selection. An unset selection admits all. */\nexport function requiredHookGroups(config: MajorModesConfig): Set<string> | undefined {\n const groups = new Set<string>();\n for (const mode of Object.values(config.majorMode)) {\n const layers = mode.layers.map((name) => config.layers[name]);\n if (layers.length === 0 || layers.some((layer) => layer?.hookGroups === undefined)) return undefined;\n for (const layer of layers) for (const group of layer.hookGroups ?? []) groups.add(group);\n }\n return groups;\n}\n\nasync function hookParser(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n}): Promise<HookParserModule | undefined> {\n const candidates = doomConfigCandidates('hooks.yaml', options.repoRoot, options.homeDirectory);\n // Command-only registries predate module compilation and do not require the package.\n // This is discovery only. The public parser still owns validation and attribution.\n const hasModuleRows = candidates.some((candidate) => {\n if (!fs.existsSync(candidate.filePath)) return false;\n const document: unknown = parseYaml(fs.readFileSync(candidate.filePath, 'utf8'));\n if (!isRecord(document) || !isRecord(document.groups)) return false;\n return Object.values(document.groups).some(\n (group) =>\n isRecord(group) &&\n Array.isArray(group.hooks) &&\n group.hooks.some((row: unknown) => isRecord(row) && isRecord(row.pi) && 'module' in row.pi),\n );\n });\n const parserEntry =\n optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.repoRoot, 'package.json')).href) ??\n (options.configRoot && options.configRoot !== options.repoRoot\n ? optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.configRoot, 'package.json')).href)\n : undefined) ??\n optionalPackageEntry('@agimon-ai/po-hook', import.meta.url);\n if (hasModuleRows && !parserEntry)\n throw new Error(\n 'Hook module rows require @agimon-ai/po-hook. Install it in the repository or select a layer that provides it, then run po sync.',\n );\n return hasModuleRows && parserEntry\n ? ((await import(pathToFileURL(parserEntry).href)) as HookParserModule)\n : undefined;\n}\n\n/** Validate the selected compiled hooks without importing repository code. */\nexport async function validateHookModules(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n hookGroups?: readonly string[];\n descriptor?: { file: string };\n isSubagent?: boolean;\n}): Promise<boolean> {\n const parser = await hookParser(options);\n if (!parser) return false;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const allowed = options.hookGroups === undefined ? undefined : new Set(options.hookGroups);\n const rows = read.entries\n .filter((row) => row.core || !allowed || allowed.has(row.groupId))\n .filter((row) => !(options.isSubagent && row.skipInSubagent));\n const modules = parser.createHookModules({ descriptor: options.descriptor });\n try {\n await modules.validate(rows);\n return rows.some((row) => row.module !== undefined);\n } finally {\n await modules.dispose();\n }\n}\n\n/** Built JavaScript loads in place or from a package snapshot; TypeScript is compiled first. */\nconst BUILT_EXTENSIONS = new Set(['.mjs', '.js', '.cjs']);\nconst TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);\n/** Pi's shared runtime must resolve to the copy this harness runs, as the sync compiler pinned it. */\nconst HOSTED_PACKAGES = new Set(['@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', '@earendil-works/pi-ai']);\n\n/**\n * Compiles a TypeScript hook into one ESM file named by the hash of its bytes.\n *\n * Relative imports are bundled, so editing any of them yields a new file and URL. Bare\n * imports are left external as the absolute file their importer resolves, or Pi's copy\n * for hosted packages, so the artifact's location does not change what they load. A bare\n * import that resolves to TypeScript is bundled, because Node does not strip it under\n * node_modules. `import.meta.url` keeps naming each original source file.\n */\nasync function compileHookSource(source: string, directory: string): Promise<string> {\n const { rolldown } = await import('rolldown');\n const build = await rolldown({\n input: source,\n platform: 'node',\n onLog(_level, log, handler) {\n if (log.code === 'UNRESOLVED_IMPORT') handler('error', log);\n },\n plugins: [\n {\n name: 'po-hook-externals',\n async resolveId(specifier, importer, extra) {\n if (!importer || specifier.startsWith('.') || path.isAbsolute(specifier)) return null;\n if (isBuiltin(specifier)) return { id: specifier, external: true };\n const root = specifier.split('/', specifier.startsWith('@') ? 2 : 1).join('/');\n const hosted = HOSTED_PACKAGES.has(root) ? optionalPackageEntry(specifier, import.meta.url) : undefined;\n const resolved = hosted ?? (await this.resolve(specifier, importer, { ...extra, skipSelf: true }))?.id;\n if (resolved === undefined || !path.isAbsolute(resolved)) return null;\n if (TYPESCRIPT_EXTENSIONS.has(path.extname(resolved))) return resolved;\n return { id: pathToFileURL(resolved).href, external: true };\n },\n transform(code, id) {\n if (!path.isAbsolute(id) || !code.includes('import.meta.url')) return null;\n return code.replaceAll(/\\bimport\\.meta\\.url\\b/gu, JSON.stringify(pathToFileURL(id).href));\n },\n },\n ],\n });\n let code: string;\n try {\n const generated = await build.generate({ format: 'esm', sourcemap: false, codeSplitting: false });\n code = generated.output[0].code;\n } finally {\n await build.close();\n }\n await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });\n const file = path.join(directory, `${crypto.createHash('sha256').update(code).digest('hex')}.mjs`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, code, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return file;\n}\n\n/**\n * Writes a hook module descriptor from the live registry.\n *\n * A built JavaScript source maps through `artifactFor` (the server's content-addressed\n * package paths); a TypeScript source is compiled into `directory`. A source that cannot\n * be prepared is left out with a notice naming its rows, so validation fails closed for\n * them. The descriptor is named by its content, so equal registries share one file.\n * Returns undefined when no registry row names a module.\n */\nexport async function writeLiveHookModules(options: {\n repoRoot: string;\n homeDirectory: string;\n directory: string;\n artifactFor: (source: string) => Promise<string>;\n onNotice?: (message: string) => void;\n}): Promise<{ file: string } | undefined> {\n const parser = await hookParser(options);\n if (!parser) return undefined;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const sources = new Map<string, Array<{ registry: string; groupId: string; rowId: string; event: string }>>();\n for (const row of read.entries) {\n if (!row.module) continue;\n const rows = sources.get(row.module) ?? [];\n rows.push({ registry: row.registryId, groupId: row.groupId, rowId: row.rowId, event: row.event });\n sources.set(row.module, rows);\n }\n if (sources.size === 0) return undefined;\n const modules = [];\n for (const [source, rows] of sources) {\n const extension = path.extname(source);\n try {\n let artifact: string;\n if (TYPESCRIPT_EXTENSIONS.has(extension)) artifact = await compileHookSource(source, options.directory);\n else if (BUILT_EXTENSIONS.has(extension)) artifact = await options.artifactFor(source);\n else throw new Error('a hook module must be TypeScript (.ts, .mts, .cts) or JavaScript (.mjs, .js, .cjs).');\n modules.push({ source, artifact, rows });\n } catch (error) {\n // Left out of the descriptor, so validation fails closed for its rows.\n const attribution = rows\n .map((row) => `${row.registry}: group ${row.groupId}, row ${row.rowId}, event ${row.event}`)\n .join('; ');\n options.onNotice?.(\n `Hook module ${source} is unavailable (${attribution}): ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n const content = `${JSON.stringify({ version: 1, modules }, null, 2)}\\n`;\n await fs.promises.mkdir(options.directory, { recursive: true, mode: 0o700 });\n const file = path.join(options.directory, `${crypto.createHash('sha256').update(content).digest('hex')}.json`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, content, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return { file };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["parseYaml"],"sources":["../../../../src/builders/hooks/index.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport { isBuiltin } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { poConfigCandidates } from '@agimon-ai/po-config/layeredConfig';\nimport type { MajorModesConfig } from '@agimon-ai/po-config/majorModes';\nimport { optionalPackageEntry } from '@agimon-ai/po-core/moduleResolution';\nimport { isRecord } from '@agimon-ai/po-core/runtimeJson';\nimport { parse as parseYaml } from 'yaml';\n\nexport const HOOK_MODULES_FILE = 'hook-modules.json';\n\n/** Add checkout source names while retaining the admitted generation's artifact mappings. */\nexport function checkoutHookDescriptor(\n descriptor: { file: string } | undefined,\n sourceRoot: string,\n repoRoot: string,\n directory: string,\n): { file: string } | undefined {\n if (!descriptor || path.resolve(sourceRoot) === path.resolve(repoRoot)) return descriptor;\n const value: unknown = JSON.parse(fs.readFileSync(descriptor.file, 'utf8'));\n if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.modules))\n throw new Error('invalid hook module descriptor');\n const sources = new Set<string>();\n const modules: Array<Record<string, unknown> & { source: string; artifact: string }> = [];\n // Validate all originals first. Never repair an invalid descriptor by dropping entries.\n for (const entry of value.modules) {\n if (\n !isRecord(entry) ||\n typeof entry.source !== 'string' ||\n typeof entry.artifact !== 'string' ||\n !path.isAbsolute(entry.source) ||\n !path.isAbsolute(entry.artifact) ||\n !['.mjs', '.js', '.cjs'].includes(path.extname(entry.artifact)) ||\n sources.has(entry.source)\n )\n throw new Error('invalid hook module artifact mapping');\n sources.add(entry.source);\n modules.push({ ...entry, source: entry.source, artifact: entry.artifact });\n }\n const aliases = [];\n for (const entry of modules) {\n const relative = path.relative(sourceRoot, entry.source);\n if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue;\n const source = path.resolve(repoRoot, relative);\n if (sources.has(source)) continue;\n sources.add(source);\n aliases.push({ ...entry, source });\n }\n if (aliases.length === 0) return descriptor;\n const file = path.join(directory, HOOK_MODULES_FILE);\n fs.writeFileSync(file, `${JSON.stringify({ ...value, modules: [...modules, ...aliases] }, null, 2)}\\n`, {\n mode: 0o600,\n });\n return { file };\n}\n\ninterface HookRegistryRow {\n module?: string;\n core: boolean;\n skipInSubagent?: boolean;\n registryId: string;\n groupId: string;\n rowId: string;\n event: string;\n}\n\n/** The optional hook package owns parsing; the CLI only consumes this capability. */\ninterface HookParserModule {\n createHookModules(options: { descriptor?: { file: string } }): {\n validate(rows: readonly HookRegistryRow[]): Promise<void>;\n dispose(): Promise<void>;\n };\n createHookDocumentReader(options: { homeDirectory: string; warn: (message: string) => void }): {\n registry(repoRoot: string): Promise<{\n entries: HookRegistryRow[];\n failure?: { message: string };\n }>;\n };\n}\n\n/** Core plus the union of every mode's selection. An unset selection admits all. */\nexport function requiredHookGroups(config: MajorModesConfig): Set<string> | undefined {\n const groups = new Set<string>();\n for (const mode of Object.values(config.majorMode)) {\n const layers = mode.layers.map((name) => config.layers[name]);\n if (layers.length === 0 || layers.some((layer) => layer?.hookGroups === undefined)) return undefined;\n for (const layer of layers) for (const group of layer.hookGroups ?? []) groups.add(group);\n }\n return groups;\n}\n\nasync function hookParser(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n}): Promise<HookParserModule | undefined> {\n const candidates = poConfigCandidates('hooks.yaml', options.repoRoot, options.homeDirectory);\n // Command-only registries predate module compilation and do not require the package.\n // This is discovery only. The public parser still owns validation and attribution.\n const hasModuleRows = candidates.some((candidate) => {\n if (!fs.existsSync(candidate.filePath)) return false;\n const document: unknown = parseYaml(fs.readFileSync(candidate.filePath, 'utf8'));\n if (!isRecord(document) || !isRecord(document.groups)) return false;\n return Object.values(document.groups).some(\n (group) =>\n isRecord(group) &&\n Array.isArray(group.hooks) &&\n group.hooks.some((row: unknown) => isRecord(row) && isRecord(row.pi) && 'module' in row.pi),\n );\n });\n const parserEntry =\n optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.repoRoot, 'package.json')).href) ??\n (options.configRoot && options.configRoot !== options.repoRoot\n ? optionalPackageEntry('@agimon-ai/po-hook', pathToFileURL(path.join(options.configRoot, 'package.json')).href)\n : undefined) ??\n optionalPackageEntry('@agimon-ai/po-hook', import.meta.url);\n if (hasModuleRows && !parserEntry)\n throw new Error(\n 'Hook module rows require @agimon-ai/po-hook. Install it in the repository or select a layer that provides it, then run po sync.',\n );\n return hasModuleRows && parserEntry\n ? ((await import(pathToFileURL(parserEntry).href)) as HookParserModule)\n : undefined;\n}\n\n/** Validate the selected compiled hooks without importing repository code. */\nexport async function validateHookModules(options: {\n repoRoot: string;\n configRoot?: string;\n homeDirectory: string;\n hookGroups?: readonly string[];\n descriptor?: { file: string };\n isSubagent?: boolean;\n}): Promise<boolean> {\n const parser = await hookParser(options);\n if (!parser) return false;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const allowed = options.hookGroups === undefined ? undefined : new Set(options.hookGroups);\n const rows = read.entries\n .filter((row) => row.core || !allowed || allowed.has(row.groupId))\n .filter((row) => !(options.isSubagent && row.skipInSubagent));\n const modules = parser.createHookModules({ descriptor: options.descriptor });\n try {\n await modules.validate(rows);\n return rows.some((row) => row.module !== undefined);\n } finally {\n await modules.dispose();\n }\n}\n\n/** Built JavaScript loads in place or from a package snapshot; TypeScript is compiled first. */\nconst BUILT_EXTENSIONS = new Set(['.mjs', '.js', '.cjs']);\nconst TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);\n/** Pi's shared runtime must resolve to the copy this harness runs, as the sync compiler pinned it. */\nconst HOSTED_PACKAGES = new Set(['@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', '@earendil-works/pi-ai']);\n\n/**\n * Compiles a TypeScript hook into one ESM file named by the hash of its bytes.\n *\n * Relative imports are bundled, so editing any of them yields a new file and URL. Bare\n * imports are left external as the absolute file their importer resolves, or Pi's copy\n * for hosted packages, so the artifact's location does not change what they load. A bare\n * import that resolves to TypeScript is bundled, because Node does not strip it under\n * node_modules. `import.meta.url` keeps naming each original source file.\n */\nasync function compileHookSource(source: string, directory: string): Promise<string> {\n const { rolldown } = await import('rolldown');\n const build = await rolldown({\n input: source,\n platform: 'node',\n onLog(_level, log, handler) {\n if (log.code === 'UNRESOLVED_IMPORT') handler('error', log);\n },\n plugins: [\n {\n name: 'po-hook-externals',\n async resolveId(specifier, importer, extra) {\n if (!importer || specifier.startsWith('.') || path.isAbsolute(specifier)) return null;\n if (isBuiltin(specifier)) return { id: specifier, external: true };\n const root = specifier.split('/', specifier.startsWith('@') ? 2 : 1).join('/');\n const hosted = HOSTED_PACKAGES.has(root) ? optionalPackageEntry(specifier, import.meta.url) : undefined;\n const resolved = hosted ?? (await this.resolve(specifier, importer, { ...extra, skipSelf: true }))?.id;\n if (resolved === undefined || !path.isAbsolute(resolved)) return null;\n if (TYPESCRIPT_EXTENSIONS.has(path.extname(resolved))) return resolved;\n return { id: pathToFileURL(resolved).href, external: true };\n },\n transform(code, id) {\n if (!path.isAbsolute(id) || !code.includes('import.meta.url')) return null;\n return code.replaceAll(/\\bimport\\.meta\\.url\\b/gu, JSON.stringify(pathToFileURL(id).href));\n },\n },\n ],\n });\n let code: string;\n try {\n const generated = await build.generate({ format: 'esm', sourcemap: false, codeSplitting: false });\n code = generated.output[0].code;\n } finally {\n await build.close();\n }\n await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });\n const file = path.join(directory, `${crypto.createHash('sha256').update(code).digest('hex')}.mjs`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, code, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return file;\n}\n\n/**\n * Writes a hook module descriptor from the live registry.\n *\n * A built JavaScript source maps through `artifactFor` (the server's content-addressed\n * package paths); a TypeScript source is compiled into `directory`. A source that cannot\n * be prepared is left out with a notice naming its rows, so validation fails closed for\n * them. The descriptor is named by its content, so equal registries share one file.\n * Returns undefined when no registry row names a module.\n */\nexport async function writeLiveHookModules(options: {\n repoRoot: string;\n homeDirectory: string;\n directory: string;\n artifactFor: (source: string) => Promise<string>;\n onNotice?: (message: string) => void;\n}): Promise<{ file: string } | undefined> {\n const parser = await hookParser(options);\n if (!parser) return undefined;\n const read = await parser\n .createHookDocumentReader({ homeDirectory: options.homeDirectory, warn: () => undefined })\n .registry(options.repoRoot);\n if (read.failure) throw new Error(read.failure.message);\n const sources = new Map<string, Array<{ registry: string; groupId: string; rowId: string; event: string }>>();\n for (const row of read.entries) {\n if (!row.module) continue;\n const rows = sources.get(row.module) ?? [];\n rows.push({ registry: row.registryId, groupId: row.groupId, rowId: row.rowId, event: row.event });\n sources.set(row.module, rows);\n }\n if (sources.size === 0) return undefined;\n const modules = [];\n for (const [source, rows] of sources) {\n const extension = path.extname(source);\n try {\n let artifact: string;\n if (TYPESCRIPT_EXTENSIONS.has(extension)) artifact = await compileHookSource(source, options.directory);\n else if (BUILT_EXTENSIONS.has(extension)) artifact = await options.artifactFor(source);\n else throw new Error('a hook module must be TypeScript (.ts, .mts, .cts) or JavaScript (.mjs, .js, .cjs).');\n modules.push({ source, artifact, rows });\n } catch (error) {\n // Left out of the descriptor, so validation fails closed for its rows.\n const attribution = rows\n .map((row) => `${row.registry}: group ${row.groupId}, row ${row.rowId}, event ${row.event}`)\n .join('; ');\n options.onNotice?.(\n `Hook module ${source} is unavailable (${attribution}): ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n const content = `${JSON.stringify({ version: 1, modules }, null, 2)}\\n`;\n await fs.promises.mkdir(options.directory, { recursive: true, mode: 0o700 });\n const file = path.join(options.directory, `${crypto.createHash('sha256').update(content).digest('hex')}.json`);\n if (!fs.existsSync(file)) {\n const staging = `${file}.${String(process.pid)}.${crypto.randomUUID()}`;\n await fs.promises.writeFile(staging, content, { mode: 0o600 });\n await fs.promises.rename(staging, file);\n }\n return { file };\n}\n"],"mappings":"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"}
@@ -198,6 +198,16 @@ function createSyncCheckpoints(input) {
198
198
  const host = () => require_index$2.syncHostIdentity(input.globalRoot, fingerprintEnvironment);
199
199
  /** The last foreign pointer (install and generation) this host warned about. */
200
200
  let warnedTakeover;
201
+ /** Each root's selection fingerprint, kept until `invalidate`: resolving it composes every mode. */
202
+ const fingerprints = /* @__PURE__ */ new Map();
203
+ const fingerprintFor = (root) => {
204
+ let fingerprint = fingerprints.get(root);
205
+ if (fingerprint === void 0) {
206
+ fingerprint = require_index$2.rootSelectionFingerprint(root, input.homeDirectory, fingerprintEnvironment);
207
+ fingerprints.set(root, fingerprint);
208
+ }
209
+ return fingerprint;
210
+ };
201
211
  /**
202
212
  * The root's state when it is stale (no pointer, a pointer another Po install wrote, a changed
203
213
  * catalogue, or a selection that differs from the pointer's), else undefined.
@@ -209,7 +219,7 @@ function createSyncCheckpoints(input) {
209
219
  const staleState = (root) => {
210
220
  const pointer = require_index$2.readSyncPointer(input.globalRoot);
211
221
  const catalogueHash = require_index$1.packageCatalogHash(input.environment);
212
- const fingerprint = require_index$2.rootSelectionFingerprint(root, input.homeDirectory, fingerprintEnvironment);
222
+ const fingerprint = fingerprintFor(root);
213
223
  if (pointer !== void 0 && pointer.host !== host()) return JSON.stringify([
214
224
  "host",
215
225
  pointer.host ?? null,
@@ -249,6 +259,8 @@ function createSyncCheckpoints(input) {
249
259
  };
250
260
  return {
251
261
  stale,
262
+ /** Forgets every root's fingerprint; the caller runs it when a root's selection may have moved. */
263
+ invalidate: () => fingerprints.clear(),
252
264
  /**
253
265
  * Whether this host may publish the builds `pointer` names: only when its own Po install wrote
254
266
  * it. Every pointer another install writes (each sync moves its generation) is refused, and
@@ -1 +1 @@
1
- {"version":3,"file":"backgroundSync.cjs","names":["HARNESS_STATE_POINTER","HARNESS_STATE_KEYS","path","poPackageRoot","fs","spawnProcess","syncHostIdentity","readSyncPointer","packageCatalogHash","rootSelectionFingerprint","readSyncLastAttempt"],"sources":["../../../../src/builders/server/backgroundSync.ts"],"sourcesContent":["import { type ChildProcess, spawn as spawnProcess, type SpawnOptions } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { HARNESS_STATE_KEYS, HARNESS_STATE_POINTER } from '../../composition/harnessState';\nimport { packageCatalogHash } from '../../composition/layerPackageInstaller';\nimport {\n readSyncLastAttempt,\n readSyncPointer,\n rootSelectionFingerprint,\n SYNC_EXIT_BUSY,\n SYNC_EXIT_OK,\n type SyncPointer,\n syncHostIdentity,\n} from '../../composition/syncService';\nimport { poPackageRoot } from '../cli/entryResolution';\n\n/**\n * Background sync: hosts that only read what sync prepared start `po sync --json --global` as\n * a child process when a root is out of date, and pick its artifacts up when it exits. The\n * terminal app instead syncs from its repository without `--global`, which registers it.\n *\n * At most one job runs per host. Requests that arrive while it runs merge into one rerun, and\n * nothing waits on a job, so admission and session open never block on sync.\n *\n * ponytail: there is no fs.watch on `.po`, so a config edit syncs at the next checkpoint\n * (server start, admission, session open). Add a watcher if edits must apply sooner.\n */\n\n/** Set on every background sync child to the pid of the host that started it. */\nexport const SYNC_PARENT_PID_ENV = 'PO_SYNC_PARENT_PID';\n/** `PO_AUTO_SYNC=0` turns background sync off, for CI and the system test. */\nexport const AUTO_SYNC_ENV = 'PO_AUTO_SYNC';\nconst AUTO_SYNC_OFF = '0';\nconst ROOT_ENV = 'PO_ROOT';\nconst SYNC_ARGS = ['sync', '--json'] as const;\nconst GLOBAL_OPTION = '--global';\nconst CLI_ENTRY_SEGMENTS = ['dist', 'bin', 'cli.mjs'] as const;\n\n/** The host pid a sync child was started by, or undefined when this process is not one. */\nexport function syncParentPid(environment: NodeJS.ProcessEnv): number | undefined {\n const pid = Number(environment[SYNC_PARENT_PID_ENV]);\n return Number.isInteger(pid) && pid > 0 ? pid : undefined;\n}\n\n/**\n * The environment a sync child runs under, and the one a host compares root fingerprints\n * with: no harness state and no root of the host's own, so both resolve every root alike.\n */\nexport function syncEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const scoped = { ...environment };\n for (const key of [ROOT_ENV, HARNESS_STATE_POINTER, ...Object.values(HARNESS_STATE_KEYS)]) delete scoped[key];\n return scoped;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport interface BackgroundSyncOptions {\n environment: NodeJS.ProcessEnv;\n onNotice: (message: string) => void;\n /** Runs after each job's child exits, before a merged rerun starts. */\n onExit?: (code: number | null) => void | Promise<void>;\n /** Lets the child outlive this process, for a terminal app that exits before sync finishes. */\n detached?: boolean;\n /**\n * The repository the child syncs from, without `--global`, so sync registers it as a workspace.\n * Without one the child runs with `--global` and registers nothing.\n */\n repoRoot?: string;\n /** Test seams. */\n spawn?: (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;\n cliEntry?: string;\n parentPid?: number;\n}\n\nexport interface BackgroundSync {\n /** Starts a job, or merges into the rerun of the running one. False when background sync is off. */\n request(reason: string): boolean;\n /** Whether a job runs or a rerun is queued. */\n readonly syncing: boolean;\n /** Resolves once no job runs and none is queued. */\n idle(): Promise<void>;\n}\n\n/** Whether this host may start background sync: on unless `PO_AUTO_SYNC=0`, and never inside a sync child. */\nexport function autoSyncEnabled(environment: NodeJS.ProcessEnv): boolean {\n return environment[AUTO_SYNC_ENV] !== AUTO_SYNC_OFF && syncParentPid(environment) === undefined;\n}\n\nexport function createBackgroundSync(options: BackgroundSyncOptions): BackgroundSync {\n const enabled = autoSyncEnabled(options.environment);\n let running = false;\n let rerun = false;\n const waiters: (() => void)[] = [];\n const settle = (): void => {\n if (!running) for (const resolve of waiters.splice(0)) resolve();\n };\n const start = (): boolean => {\n const entry = options.cliEntry ?? path.join(poPackageRoot(), ...CLI_ENTRY_SEGMENTS);\n if (!fs.existsSync(entry)) {\n options.onNotice(`Background sync is unavailable because '${entry}' is missing; run po sync.`);\n return false;\n }\n let child: ChildProcess;\n try {\n // On desktop the child inherits ELECTRON_RUN_AS_NODE, so the app binary runs it as node.\n const scope = options.repoRoot === undefined ? [GLOBAL_OPTION] : [];\n child = (options.spawn ?? spawnProcess)(process.execPath, [...process.execArgv, entry, ...SYNC_ARGS, ...scope], {\n ...(options.repoRoot === undefined ? {} : { cwd: options.repoRoot }),\n env: {\n ...syncEnvironment(options.environment),\n [SYNC_PARENT_PID_ENV]: String(options.parentPid ?? process.pid),\n },\n stdio: 'ignore',\n detached: options.detached === true,\n windowsHide: true,\n });\n } catch (error) {\n options.onNotice(`Background sync could not start: ${errorMessage(error)}`);\n return false;\n }\n running = true;\n if (options.detached === true) child.unref();\n let finished = false;\n const finish = (code: number | null): void => {\n if (finished) return;\n finished = true;\n if (code !== null && code !== SYNC_EXIT_OK && code !== SYNC_EXIT_BUSY)\n options.onNotice(`Background sync exited with code ${String(code)}.`);\n void Promise.resolve()\n .then(() => options.onExit?.(code))\n .catch((error: unknown) => options.onNotice(`Picking up background sync failed: ${errorMessage(error)}`))\n .finally(() => {\n running = false;\n if (rerun) {\n rerun = false;\n start();\n }\n settle();\n });\n };\n child.once('error', (error) => {\n options.onNotice(`Background sync could not start: ${error.message}`);\n finish(null);\n });\n child.once('exit', (code) => finish(code));\n return true;\n };\n return {\n request(): boolean {\n if (!enabled) return false;\n if (running) {\n rerun = true;\n return true;\n }\n return start();\n },\n get syncing() {\n return running || rerun;\n },\n idle() {\n return new Promise<void>((resolve) => {\n waiters.push(resolve);\n settle();\n });\n },\n };\n}\n\n/**\n * Publishes a build only when the key it is given differs from the last one published, so a\n * sync that changed nothing republishes nothing. A key whose build cannot be read is skipped.\n */\nexport function createKeyedPickup<T>(\n read: (key: string) => T | undefined,\n publish: (build: T, key: string) => void,\n): (key: string | null | undefined) => 'published' | 'unchanged' | 'missing' {\n let current: string | undefined;\n return (key) => {\n if (key === null || key === undefined || key === current) return 'unchanged';\n const build = read(key);\n if (build === undefined) return 'missing';\n current = key;\n publish(build, key);\n return 'published';\n };\n}\n\nexport interface SyncedBuildKeys {\n webKey: string | null | undefined;\n nativeKey: string | null | undefined;\n}\n\nexport interface SyncedBuildKeysInput {\n pointer: SyncPointer | undefined;\n /** Whether this host's own Po install wrote `pointer`. */\n ownsPointer: boolean;\n /** The kinds this host already publishes. */\n published: { web: boolean; native: boolean };\n /** The keys this install derives the way sync does, each only when its build exists. */\n ownKeys: () => { webKey?: string; nativeKey?: string };\n /** The newest web build in the Po home, whichever install made it. */\n newestWebKey: () => string | undefined;\n}\n\n/**\n * The build keys a host publishes. Its own pointer names them. Before sync ever wrote one (the\n * first start after an upgrade), this install's derived keys, else the newest web build. A\n * pointer another Po install wrote is refused: what the host already publishes stays (an\n * undefined key changes nothing), and a kind it has not published yet takes only this\n * install's own build while its own sync runs, never the other install's.\n */\nexport function syncedBuildKeys(input: SyncedBuildKeysInput): SyncedBuildKeys {\n const { pointer, published } = input;\n if (input.ownsPointer && pointer !== undefined) return { webKey: pointer.webKey, nativeKey: pointer.nativeKey };\n if (pointer === undefined) {\n const own = input.ownKeys();\n return { webKey: own.webKey ?? input.newestWebKey(), nativeKey: own.nativeKey };\n }\n if (published.web && published.native) return { webKey: undefined, nativeKey: undefined };\n const own = input.ownKeys();\n return {\n webKey: published.web ? undefined : own.webKey,\n nativeKey: published.native ? undefined : own.nativeKey,\n };\n}\n\nexport interface SyncCheckpointsInput {\n globalRoot: string;\n homeDirectory: string;\n environment: NodeJS.ProcessEnv;\n background: BackgroundSync;\n onNotice: (message: string) => void;\n}\n\nexport interface RootSyncCheck {\n root: string;\n /** How the root is named in a warning, such as `Workspace 'po'`. */\n label: string;\n /** The selectors this host left out because they are not synced. */\n unsynced: readonly string[];\n}\n\nexport interface RootSyncStatus {\n syncing: boolean;\n /** One line per unsynced selector: why sync could not prepare it, or that it is still syncing. */\n reasons: string[];\n}\n\n/**\n * The checkpoints a host runs when it admits a root or opens a session: request a background\n * job when the root changed since the last sync, and warn about what it opened without. Each\n * root's warning is emitted once until it changes.\n */\nexport function createSyncCheckpoints(input: SyncCheckpointsInput) {\n const fingerprintEnvironment = syncEnvironment(input.environment);\n const warned = new Map<string, string>();\n /** The state each root last requested a sync for: pointer generation, catalogue and selection. */\n const requested = new Map<string, string>();\n /** This host's install, compared with the one that wrote the pointer. */\n const host = (): string => syncHostIdentity(input.globalRoot, fingerprintEnvironment);\n /** The last foreign pointer (install and generation) this host warned about. */\n let warnedTakeover: string | undefined;\n /**\n * The root's state when it is stale (no pointer, a pointer another Po install wrote, a changed\n * catalogue, or a selection that differs from the pointer's), else undefined.\n *\n * A foreign pointer's state includes its generation, which every sync moves, so each takeover\n * of the shared runtime is one new stale state and gets one resync. That settles: the resync\n * writes this host's own pointer, which is never stale for being foreign.\n */\n const staleState = (root: string): string | undefined => {\n const pointer = readSyncPointer(input.globalRoot);\n const catalogueHash = packageCatalogHash(input.environment);\n const fingerprint = rootSelectionFingerprint(root, input.homeDirectory, fingerprintEnvironment);\n if (pointer !== undefined && pointer.host !== host())\n return JSON.stringify(['host', pointer.host ?? null, pointer.generation, catalogueHash, fingerprint]);\n if (pointer?.catalogueHash === catalogueHash && pointer.fingerprints?.[root] === fingerprint) return undefined;\n return JSON.stringify([pointer?.generation ?? null, catalogueHash, fingerprint]);\n };\n const stale = (root: string): boolean => staleState(root) !== undefined;\n /**\n * Requests one sync per stale state of a root. A sync that ran and could not change the\n * pointer (it failed) is not requested again until the root, the catalogue or the pointer\n * moves, so a broken root never makes every checkpoint start another full sync.\n */\n const requestIfStale = (check: RootSyncCheck): boolean => {\n const state = staleState(check.root);\n if (state === undefined) return false;\n if (requested.get(check.root) === state) return true;\n if (!input.background.request(`${check.label} changed since the last sync`)) return false;\n requested.set(check.root, state);\n return true;\n };\n /** The missing artifacts a sync was requested for, each with the pointer state it was missing under. */\n const requestedMissing = new Set<string>();\n /** Why the last attempt, or failing that the pointer, could not prepare each selector. */\n const failures = (root: string): Map<string, string> => {\n const reports = [readSyncLastAttempt(input.globalRoot), readSyncPointer(input.globalRoot)];\n const reasons = new Map<string, string>();\n for (const report of reports)\n for (const entry of report?.roots.find((candidate) => candidate.root === root)?.unsynced ?? [])\n if (!reasons.has(entry.selector)) reasons.set(entry.selector, entry.reason);\n return reasons;\n };\n return {\n stale,\n /**\n * Whether this host may publish the builds `pointer` names: only when its own Po install wrote\n * it. Every pointer another install writes (each sync moves its generation) is refused, and\n * gets one background sync of the global root and one warning, so this host keeps its previous\n * publication until its own sync replaces the pointer.\n */\n ownsPointer(pointer: SyncPointer | undefined): pointer is SyncPointer {\n if (pointer === undefined) return false;\n const own = host();\n if (pointer.host === own) return true;\n const foreign = pointer.host ?? 'an older Po';\n const resyncing = requestIfStale({ root: input.globalRoot, label: 'The global scope', unsynced: [] });\n const takeover = JSON.stringify([foreign, pointer.generation]);\n if (warnedTakeover !== takeover) {\n warnedTakeover = takeover;\n input.onNotice(\n `Another Po install (${foreign}) replaced the shared packages; ${resyncing ? `resyncing with this install (${own})` : 'run po sync from this install to resync'}. Use one Po install so they stop replacing each other.`,\n );\n }\n return false;\n },\n /**\n * Requests one sync per missing artifact (such as an MCP app build key) and pointer state,\n * the same limit stale roots get: a sync that ran and still did not produce it is not\n * requested again until the pointer or the catalogue moves.\n */\n requestMissing(artifact: string, reason: string): boolean {\n const pointer = readSyncPointer(input.globalRoot);\n const state = JSON.stringify([artifact, pointer?.generation ?? null, packageCatalogHash(input.environment)]);\n if (requestedMissing.has(state) || !input.background.request(reason)) return false;\n requestedMissing.add(state);\n return true;\n },\n /** Requests a job when `request` allows and the root is stale, then warns about `unsynced`. */\n check(check: RootSyncCheck, request = true): RootSyncStatus {\n if (request) requestIfStale(check);\n const syncing = input.background.syncing;\n if (check.unsynced.length === 0) {\n warned.delete(check.root);\n return { syncing, reasons: [] };\n }\n const known = failures(check.root);\n const count = check.unsynced.length;\n const noun = count === 1 ? 'extension' : 'extensions';\n const reasons = check.unsynced.map((selector) => {\n const reason = known.get(selector);\n if (syncing || reason === undefined) return `${selector} is not synced yet`;\n return `Sync could not prepare ${selector}: ${reason}.`;\n });\n const message = syncing\n ? `${check.label} opened without ${String(count)} ${noun} that ${count === 1 ? 'is' : 'are'} still syncing: ${check.unsynced.join(', ')}. ${count === 1 ? 'It loads' : 'They load'} when sync finishes.`\n : check.unsynced.every((selector) => known.has(selector))\n ? reasons.join(' ')\n : `${check.label} opened without ${String(count)} ${noun} that ${count === 1 ? 'is' : 'are'} not synced: ${check.unsynced.join(', ')}. Run po sync to prepare them.`;\n if (warned.get(check.root) !== message) {\n warned.set(check.root, message);\n input.onNotice(message);\n }\n return { syncing, reasons };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"backgroundSync.cjs","names":["HARNESS_STATE_POINTER","HARNESS_STATE_KEYS","path","poPackageRoot","fs","spawnProcess","syncHostIdentity","rootSelectionFingerprint","readSyncPointer","packageCatalogHash","readSyncLastAttempt"],"sources":["../../../../src/builders/server/backgroundSync.ts"],"sourcesContent":["import { type ChildProcess, spawn as spawnProcess, type SpawnOptions } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { HARNESS_STATE_KEYS, HARNESS_STATE_POINTER } from '../../composition/harnessState';\nimport { packageCatalogHash } from '../../composition/layerPackageInstaller';\nimport {\n readSyncLastAttempt,\n readSyncPointer,\n rootSelectionFingerprint,\n SYNC_EXIT_BUSY,\n SYNC_EXIT_OK,\n type SyncPointer,\n syncHostIdentity,\n} from '../../composition/syncService';\nimport { poPackageRoot } from '../cli/entryResolution';\n\n/**\n * Background sync: hosts that only read what sync prepared start `po sync --json --global` as\n * a child process when a root is out of date, and pick its artifacts up when it exits. The\n * terminal app instead syncs from its repository without `--global`, which registers it.\n *\n * At most one job runs per host. Requests that arrive while it runs merge into one rerun, and\n * nothing waits on a job, so admission and session open never block on sync.\n *\n * ponytail: there is no fs.watch on `.po`, so a config edit syncs at the next checkpoint\n * (server start, admission, session open). Add a watcher if edits must apply sooner.\n */\n\n/** Set on every background sync child to the pid of the host that started it. */\nexport const SYNC_PARENT_PID_ENV = 'PO_SYNC_PARENT_PID';\n/** `PO_AUTO_SYNC=0` turns background sync off, for CI and the system test. */\nexport const AUTO_SYNC_ENV = 'PO_AUTO_SYNC';\nconst AUTO_SYNC_OFF = '0';\nconst ROOT_ENV = 'PO_ROOT';\nconst SYNC_ARGS = ['sync', '--json'] as const;\nconst GLOBAL_OPTION = '--global';\nconst CLI_ENTRY_SEGMENTS = ['dist', 'bin', 'cli.mjs'] as const;\n\n/** The host pid a sync child was started by, or undefined when this process is not one. */\nexport function syncParentPid(environment: NodeJS.ProcessEnv): number | undefined {\n const pid = Number(environment[SYNC_PARENT_PID_ENV]);\n return Number.isInteger(pid) && pid > 0 ? pid : undefined;\n}\n\n/**\n * The environment a sync child runs under, and the one a host compares root fingerprints\n * with: no harness state and no root of the host's own, so both resolve every root alike.\n */\nexport function syncEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const scoped = { ...environment };\n for (const key of [ROOT_ENV, HARNESS_STATE_POINTER, ...Object.values(HARNESS_STATE_KEYS)]) delete scoped[key];\n return scoped;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport interface BackgroundSyncOptions {\n environment: NodeJS.ProcessEnv;\n onNotice: (message: string) => void;\n /** Runs after each job's child exits, before a merged rerun starts. */\n onExit?: (code: number | null) => void | Promise<void>;\n /** Lets the child outlive this process, for a terminal app that exits before sync finishes. */\n detached?: boolean;\n /**\n * The repository the child syncs from, without `--global`, so sync registers it as a workspace.\n * Without one the child runs with `--global` and registers nothing.\n */\n repoRoot?: string;\n /** Test seams. */\n spawn?: (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;\n cliEntry?: string;\n parentPid?: number;\n}\n\nexport interface BackgroundSync {\n /** Starts a job, or merges into the rerun of the running one. False when background sync is off. */\n request(reason: string): boolean;\n /** Whether a job runs or a rerun is queued. */\n readonly syncing: boolean;\n /** Resolves once no job runs and none is queued. */\n idle(): Promise<void>;\n}\n\n/** Whether this host may start background sync: on unless `PO_AUTO_SYNC=0`, and never inside a sync child. */\nexport function autoSyncEnabled(environment: NodeJS.ProcessEnv): boolean {\n return environment[AUTO_SYNC_ENV] !== AUTO_SYNC_OFF && syncParentPid(environment) === undefined;\n}\n\nexport function createBackgroundSync(options: BackgroundSyncOptions): BackgroundSync {\n const enabled = autoSyncEnabled(options.environment);\n let running = false;\n let rerun = false;\n const waiters: (() => void)[] = [];\n const settle = (): void => {\n if (!running) for (const resolve of waiters.splice(0)) resolve();\n };\n const start = (): boolean => {\n const entry = options.cliEntry ?? path.join(poPackageRoot(), ...CLI_ENTRY_SEGMENTS);\n if (!fs.existsSync(entry)) {\n options.onNotice(`Background sync is unavailable because '${entry}' is missing; run po sync.`);\n return false;\n }\n let child: ChildProcess;\n try {\n // On desktop the child inherits ELECTRON_RUN_AS_NODE, so the app binary runs it as node.\n const scope = options.repoRoot === undefined ? [GLOBAL_OPTION] : [];\n child = (options.spawn ?? spawnProcess)(process.execPath, [...process.execArgv, entry, ...SYNC_ARGS, ...scope], {\n ...(options.repoRoot === undefined ? {} : { cwd: options.repoRoot }),\n env: {\n ...syncEnvironment(options.environment),\n [SYNC_PARENT_PID_ENV]: String(options.parentPid ?? process.pid),\n },\n stdio: 'ignore',\n detached: options.detached === true,\n windowsHide: true,\n });\n } catch (error) {\n options.onNotice(`Background sync could not start: ${errorMessage(error)}`);\n return false;\n }\n running = true;\n if (options.detached === true) child.unref();\n let finished = false;\n const finish = (code: number | null): void => {\n if (finished) return;\n finished = true;\n if (code !== null && code !== SYNC_EXIT_OK && code !== SYNC_EXIT_BUSY)\n options.onNotice(`Background sync exited with code ${String(code)}.`);\n void Promise.resolve()\n .then(() => options.onExit?.(code))\n .catch((error: unknown) => options.onNotice(`Picking up background sync failed: ${errorMessage(error)}`))\n .finally(() => {\n running = false;\n if (rerun) {\n rerun = false;\n start();\n }\n settle();\n });\n };\n child.once('error', (error) => {\n options.onNotice(`Background sync could not start: ${error.message}`);\n finish(null);\n });\n child.once('exit', (code) => finish(code));\n return true;\n };\n return {\n request(): boolean {\n if (!enabled) return false;\n if (running) {\n rerun = true;\n return true;\n }\n return start();\n },\n get syncing() {\n return running || rerun;\n },\n idle() {\n return new Promise<void>((resolve) => {\n waiters.push(resolve);\n settle();\n });\n },\n };\n}\n\n/**\n * Publishes a build only when the key it is given differs from the last one published, so a\n * sync that changed nothing republishes nothing. A key whose build cannot be read is skipped.\n */\nexport function createKeyedPickup<T>(\n read: (key: string) => T | undefined,\n publish: (build: T, key: string) => void,\n): (key: string | null | undefined) => 'published' | 'unchanged' | 'missing' {\n let current: string | undefined;\n return (key) => {\n if (key === null || key === undefined || key === current) return 'unchanged';\n const build = read(key);\n if (build === undefined) return 'missing';\n current = key;\n publish(build, key);\n return 'published';\n };\n}\n\nexport interface SyncedBuildKeys {\n webKey: string | null | undefined;\n nativeKey: string | null | undefined;\n}\n\nexport interface SyncedBuildKeysInput {\n pointer: SyncPointer | undefined;\n /** Whether this host's own Po install wrote `pointer`. */\n ownsPointer: boolean;\n /** The kinds this host already publishes. */\n published: { web: boolean; native: boolean };\n /** The keys this install derives the way sync does, each only when its build exists. */\n ownKeys: () => { webKey?: string; nativeKey?: string };\n /** The newest web build in the Po home, whichever install made it. */\n newestWebKey: () => string | undefined;\n}\n\n/**\n * The build keys a host publishes. Its own pointer names them. Before sync ever wrote one (the\n * first start after an upgrade), this install's derived keys, else the newest web build. A\n * pointer another Po install wrote is refused: what the host already publishes stays (an\n * undefined key changes nothing), and a kind it has not published yet takes only this\n * install's own build while its own sync runs, never the other install's.\n */\nexport function syncedBuildKeys(input: SyncedBuildKeysInput): SyncedBuildKeys {\n const { pointer, published } = input;\n if (input.ownsPointer && pointer !== undefined) return { webKey: pointer.webKey, nativeKey: pointer.nativeKey };\n if (pointer === undefined) {\n const own = input.ownKeys();\n return { webKey: own.webKey ?? input.newestWebKey(), nativeKey: own.nativeKey };\n }\n if (published.web && published.native) return { webKey: undefined, nativeKey: undefined };\n const own = input.ownKeys();\n return {\n webKey: published.web ? undefined : own.webKey,\n nativeKey: published.native ? undefined : own.nativeKey,\n };\n}\n\nexport interface SyncCheckpointsInput {\n globalRoot: string;\n homeDirectory: string;\n environment: NodeJS.ProcessEnv;\n background: BackgroundSync;\n onNotice: (message: string) => void;\n}\n\nexport interface RootSyncCheck {\n root: string;\n /** How the root is named in a warning, such as `Workspace 'po'`. */\n label: string;\n /** The selectors this host left out because they are not synced. */\n unsynced: readonly string[];\n}\n\nexport interface RootSyncStatus {\n syncing: boolean;\n /** One line per unsynced selector: why sync could not prepare it, or that it is still syncing. */\n reasons: string[];\n}\n\n/**\n * The checkpoints a host runs when it admits a root or opens a session: request a background\n * job when the root changed since the last sync, and warn about what it opened without. Each\n * root's warning is emitted once until it changes.\n */\nexport function createSyncCheckpoints(input: SyncCheckpointsInput) {\n const fingerprintEnvironment = syncEnvironment(input.environment);\n const warned = new Map<string, string>();\n /** The state each root last requested a sync for: pointer generation, catalogue and selection. */\n const requested = new Map<string, string>();\n /** This host's install, compared with the one that wrote the pointer. */\n const host = (): string => syncHostIdentity(input.globalRoot, fingerprintEnvironment);\n /** The last foreign pointer (install and generation) this host warned about. */\n let warnedTakeover: string | undefined;\n /** Each root's selection fingerprint, kept until `invalidate`: resolving it composes every mode. */\n const fingerprints = new Map<string, string>();\n const fingerprintFor = (root: string): string => {\n let fingerprint = fingerprints.get(root);\n if (fingerprint === undefined) {\n fingerprint = rootSelectionFingerprint(root, input.homeDirectory, fingerprintEnvironment);\n fingerprints.set(root, fingerprint);\n }\n return fingerprint;\n };\n /**\n * The root's state when it is stale (no pointer, a pointer another Po install wrote, a changed\n * catalogue, or a selection that differs from the pointer's), else undefined.\n *\n * A foreign pointer's state includes its generation, which every sync moves, so each takeover\n * of the shared runtime is one new stale state and gets one resync. That settles: the resync\n * writes this host's own pointer, which is never stale for being foreign.\n */\n const staleState = (root: string): string | undefined => {\n const pointer = readSyncPointer(input.globalRoot);\n const catalogueHash = packageCatalogHash(input.environment);\n const fingerprint = fingerprintFor(root);\n if (pointer !== undefined && pointer.host !== host())\n return JSON.stringify(['host', pointer.host ?? null, pointer.generation, catalogueHash, fingerprint]);\n if (pointer?.catalogueHash === catalogueHash && pointer.fingerprints?.[root] === fingerprint) return undefined;\n return JSON.stringify([pointer?.generation ?? null, catalogueHash, fingerprint]);\n };\n const stale = (root: string): boolean => staleState(root) !== undefined;\n /**\n * Requests one sync per stale state of a root. A sync that ran and could not change the\n * pointer (it failed) is not requested again until the root, the catalogue or the pointer\n * moves, so a broken root never makes every checkpoint start another full sync.\n */\n const requestIfStale = (check: RootSyncCheck): boolean => {\n const state = staleState(check.root);\n if (state === undefined) return false;\n if (requested.get(check.root) === state) return true;\n if (!input.background.request(`${check.label} changed since the last sync`)) return false;\n requested.set(check.root, state);\n return true;\n };\n /** The missing artifacts a sync was requested for, each with the pointer state it was missing under. */\n const requestedMissing = new Set<string>();\n /** Why the last attempt, or failing that the pointer, could not prepare each selector. */\n const failures = (root: string): Map<string, string> => {\n const reports = [readSyncLastAttempt(input.globalRoot), readSyncPointer(input.globalRoot)];\n const reasons = new Map<string, string>();\n for (const report of reports)\n for (const entry of report?.roots.find((candidate) => candidate.root === root)?.unsynced ?? [])\n if (!reasons.has(entry.selector)) reasons.set(entry.selector, entry.reason);\n return reasons;\n };\n return {\n stale,\n /** Forgets every root's fingerprint; the caller runs it when a root's selection may have moved. */\n invalidate: (): void => fingerprints.clear(),\n /**\n * Whether this host may publish the builds `pointer` names: only when its own Po install wrote\n * it. Every pointer another install writes (each sync moves its generation) is refused, and\n * gets one background sync of the global root and one warning, so this host keeps its previous\n * publication until its own sync replaces the pointer.\n */\n ownsPointer(pointer: SyncPointer | undefined): pointer is SyncPointer {\n if (pointer === undefined) return false;\n const own = host();\n if (pointer.host === own) return true;\n const foreign = pointer.host ?? 'an older Po';\n const resyncing = requestIfStale({ root: input.globalRoot, label: 'The global scope', unsynced: [] });\n const takeover = JSON.stringify([foreign, pointer.generation]);\n if (warnedTakeover !== takeover) {\n warnedTakeover = takeover;\n input.onNotice(\n `Another Po install (${foreign}) replaced the shared packages; ${resyncing ? `resyncing with this install (${own})` : 'run po sync from this install to resync'}. Use one Po install so they stop replacing each other.`,\n );\n }\n return false;\n },\n /**\n * Requests one sync per missing artifact (such as an MCP app build key) and pointer state,\n * the same limit stale roots get: a sync that ran and still did not produce it is not\n * requested again until the pointer or the catalogue moves.\n */\n requestMissing(artifact: string, reason: string): boolean {\n const pointer = readSyncPointer(input.globalRoot);\n const state = JSON.stringify([artifact, pointer?.generation ?? null, packageCatalogHash(input.environment)]);\n if (requestedMissing.has(state) || !input.background.request(reason)) return false;\n requestedMissing.add(state);\n return true;\n },\n /** Requests a job when `request` allows and the root is stale, then warns about `unsynced`. */\n check(check: RootSyncCheck, request = true): RootSyncStatus {\n if (request) requestIfStale(check);\n const syncing = input.background.syncing;\n if (check.unsynced.length === 0) {\n warned.delete(check.root);\n return { syncing, reasons: [] };\n }\n const known = failures(check.root);\n const count = check.unsynced.length;\n const noun = count === 1 ? 'extension' : 'extensions';\n const reasons = check.unsynced.map((selector) => {\n const reason = known.get(selector);\n if (syncing || reason === undefined) return `${selector} is not synced yet`;\n return `Sync could not prepare ${selector}: ${reason}.`;\n });\n const message = syncing\n ? `${check.label} opened without ${String(count)} ${noun} that ${count === 1 ? 'is' : 'are'} still syncing: ${check.unsynced.join(', ')}. ${count === 1 ? 'It loads' : 'They load'} when sync finishes.`\n : check.unsynced.every((selector) => known.has(selector))\n ? reasons.join(' ')\n : `${check.label} opened without ${String(count)} ${noun} that ${count === 1 ? 'is' : 'are'} not synced: ${check.unsynced.join(', ')}. Run po sync to prepare them.`;\n if (warned.get(check.root) !== message) {\n warned.set(check.root, message);\n input.onNotice(message);\n }\n return { syncing, reasons };\n },\n };\n}\n"],"mappings":"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"}
@@ -193,6 +193,16 @@ function createSyncCheckpoints(input) {
193
193
  const host = () => syncHostIdentity(input.globalRoot, fingerprintEnvironment);
194
194
  /** The last foreign pointer (install and generation) this host warned about. */
195
195
  let warnedTakeover;
196
+ /** Each root's selection fingerprint, kept until `invalidate`: resolving it composes every mode. */
197
+ const fingerprints = /* @__PURE__ */ new Map();
198
+ const fingerprintFor = (root) => {
199
+ let fingerprint = fingerprints.get(root);
200
+ if (fingerprint === void 0) {
201
+ fingerprint = rootSelectionFingerprint(root, input.homeDirectory, fingerprintEnvironment);
202
+ fingerprints.set(root, fingerprint);
203
+ }
204
+ return fingerprint;
205
+ };
196
206
  /**
197
207
  * The root's state when it is stale (no pointer, a pointer another Po install wrote, a changed
198
208
  * catalogue, or a selection that differs from the pointer's), else undefined.
@@ -204,7 +214,7 @@ function createSyncCheckpoints(input) {
204
214
  const staleState = (root) => {
205
215
  const pointer = readSyncPointer(input.globalRoot);
206
216
  const catalogueHash = packageCatalogHash(input.environment);
207
- const fingerprint = rootSelectionFingerprint(root, input.homeDirectory, fingerprintEnvironment);
217
+ const fingerprint = fingerprintFor(root);
208
218
  if (pointer !== void 0 && pointer.host !== host()) return JSON.stringify([
209
219
  "host",
210
220
  pointer.host ?? null,
@@ -244,6 +254,8 @@ function createSyncCheckpoints(input) {
244
254
  };
245
255
  return {
246
256
  stale,
257
+ /** Forgets every root's fingerprint; the caller runs it when a root's selection may have moved. */
258
+ invalidate: () => fingerprints.clear(),
247
259
  /**
248
260
  * Whether this host may publish the builds `pointer` names: only when its own Po install wrote
249
261
  * it. Every pointer another install writes (each sync moves its generation) is refused, and